diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx
index f9af1045a8..fb694c42f6 100644
--- a/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx
+++ b/apps/gittensory-ui/src/routes/docs.self-hosting-ai-providers.tsx
@@ -69,6 +69,28 @@ function SelfHostingAiProviders() {
ANTHROPIC_API_KEY=
+
+ Distinct from OpenAI-compatible below: this is the native OpenAI API path (
+
+
+
+ Set
A comma-list is a fallback chain by default. Use this for subscription CLIs when you want
diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx
index 86c5dd4358..501d4a1af6 100644
--- a/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx
+++ b/apps/gittensory-ui/src/routes/docs.self-hosting-configuration.tsx
@@ -65,6 +65,41 @@ function SelfHostingConfiguration() {
]}
/>
+
+ Where policy for a given repo can live is one question; which layer wins when more than one
+ is set is another. Most specific wins, in this order:
+
+ Within
+ This page covers the environment layer and the shape of the config file. For the full field
+ list — every ANTHROPIC_AI_BASE_URL defaults to https://api.anthropic.com — set
+ it only to route through a gateway or proxy in front of the real Anthropic API.
+ OpenAI API
+ AI_PROVIDER=openai), for when you have an OpenAI account key rather than a
+ gateway or local endpoint.
+ OPENAI_AI_BASE_URL and OPENAI_AI_MODEL already default to the
+ values shown — set them only to override the endpoint or model.
+ OpenAI-compatible endpoint
Ollama (dedicated provider)
+ AI_PROVIDER=ollama is a separate provider id from routing Ollama through{" "}
+ openai-compatible above — use whichever matches how you want
+ fallback/dual-review chains to identify it. Defaults to a local Ollama at{" "}
+ http://localhost:11434/v1 with no API key.
+ OLLAMA_AI_BASE_URL to http://ollama:11434/v1 when using the
+ compose ollama profile; OLLAMA_AI_API_KEY is normally left blank
+ for a local, unauthenticated Ollama instance.
+ Fallback and dual review
Precedence
+
+
+ .gittensory.yml (public repo config, or the mounted private
+ per-repo config file below if GITTENSORY_REPO_CONFIG_DIR is set), then
+ .gittensory.yml itself, the typed gate: block is an alias
+ for the gate-related fields and wins over the generic settings: block for those
+ same fields — so a value written under both gate.duplicates and{" "}
+ settings.duplicates resolves to whatever gate.duplicates says. One
+ exception to the whole precedence chain: hard path guardrails (
+ settings.hardGuardrailGlobs) are config-as-code only — omitted or empty means
+ no path guardrails, never a hidden engine fallback, regardless of what the database row or
+ defaults would otherwise imply.
+ gate: and settings: key, its default, and what it
+ does — see Tuning your reviews, and for a complete,
+ commented, copy-pasteable manifest see{" "}
+
+ .gittensory.yml.example
+ {" "}
+ in the repo — the authoritative reference for every field, including several documented only
+ in its comments (see below).
+ Required baseline env
FOO_FILE is loaded into FOO at startup. Explicit{" "}
FOO wins over the file variant.
+ Every command example on these docs pages hardcodes :8787 — that's the
+ default, not a fixed port. Set PORT to listen on something else; update your
+ compose port mapping and any curl/health-check commands to match.
+
GITTENSORY_MCP_TOKEN is a shared, end-user-obtainable CLI credential (the
normal alternative to gittensory-mcp login), so it must not implicitly stage
@@ -109,6 +149,33 @@ MCP_ACTUATION_REPO_ALLOWLIST=owner/repo-one, owner/repo-two
contributor/operator tools.
+ MIGRATIONS_DIR (default migrations) — where the self-host
+ runtime looks for SQL migration files to auto-apply at boot. Only relevant for a custom
+ build that ships migrations somewhere other than the default in-image location.
+ REVIEW_AUDIT_DIR — when set, persists visual-review screenshot PNGs to this
+ filesystem path so they're served from cache instead of re-rendered on every request.
+ Unset means each screenshot is re-rendered on demand. Only relevant when{" "}
+ BROWSER_WS_ENDPOINT (see{" "}
+ REES enrichment) is also set — visual review is
+ fully inert without it.
+ CODEX_HOME — do not set this for the app container. The Codex provider
+ rejects a container-set CODEX_HOME outright (fails closed with{" "}
+ codex_credential_isolation_required) because codex exec reads
+ attacker-controlled PR title/body/diff text, and a mounted OAuth home on the same
+ filesystem could otherwise leak into review output via prompt injection. This is why the
+ Codex subscription path additionally requires the explicit{" "}
+ GITTENSORY_ENABLE_UNSAFE_CODEX_REVIEWER=1 opt-in — see{" "}
+ AI providers.
+ Redis backs shared caching for stable GitHub GET responses, including repeated installation, @@ -132,6 +199,101 @@ GITHUB_METADATA_CACHE_TTL_SECONDS=600`} self-host Grafana dashboard includes the hit/miss/coalesced/error breakdown.
+ CRON_INTERVAL_MS (default 120000, ~2 minutes) is the tick that
+ drives the maintain/sweep and sync cadence — contributor evidence, burden forecasts, RAG
+ re-indexing, drift scans, and notifications all fan out from it.{" "}
+ QUEUE_BACKGROUND_CONCURRENCY (default 1) caps how many
+ low-priority background jobs may occupy a QUEUE_CONCURRENCY slot at once,
+ independent of live webhook/review work.
+
+ QUEUE_STARTUP_JITTER_MIN_JOBS (default 8) sets the pending-job
+ count below which the queue skips its startup jitter delay — useful on a small instance
+ where you'd rather a handful of jobs start processing immediately after boot than wait out a
+ jitter window meant to stagger many instances restarting at once.
+
+ Two independent, opt-out admission checks run at queue-claim time, on top of GitHub + rate-limit deferral, so background work never starves live PR review or overloads the host. + Both grew out of real production incidents — an un-jittered cron enqueue and an unbounded + per-installation background fan-out — and every value below is optional with a sane default. +
+
+ Tune MAINTENANCE_ADMISSION_MAX_LIVE_PENDING (default 5),{" "}
+ MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS (default 120000),{" "}
+ MAINTENANCE_ADMISSION_MAX_PENDING (default 15),{" "}
+ MAINTENANCE_ADMISSION_MAX_HOST_LOAD (default 1.5, a 1-minute
+ load-average-per-core ceiling), and{" "}
+ MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING (default 10)
+ if you register many repos or run a busy instance and see maintenance sweeps lagging behind
+ where you'd like. A denial backs off by MAINTENANCE_ADMISSION_DEFER_MS (default{" "}
+ 180000, 3 minutes) before jitter, but two escape hatches stop a deferral from
+ becoming a starve: MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS (default{" "}
+ 14400000, 4 hours) force-admits any maintenance job that has waited this long
+ regardless of pressure, and the shorter MAINTENANCE_ADMISSION_DRAIN_AGE_MS{" "}
+ (default 600000, 10 minutes, clamped to the 4-hour ceiling) specifically drains
+ the oldest jobs in a backed-up maintenance_pending_high lane so it can actually
+ shrink instead of denying every claim for hours. Set{" "}
+ MAINTENANCE_ADMISSION_ENABLED=false to fully disable the policy and return to
+ the old always-run behavior.
+
+ GITHUB_INSTALLATION_CONCURRENCY_LIMIT (default 2) is the per-
+ installation ceiling; GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS (default{" "}
+ 15000, 15 seconds) is its base backoff before jitter. Raise the limit if a
+ single large installation's background work is being throttled and you have GitHub
+ rate-limit and host headroom to spare; set{" "}
+ GITHUB_INSTALLATION_CONCURRENCY_ENABLED=false to disable the check entirely.
+ This check only applies to background jobs that call GitHub — live PR review (
+ github-webhook/agent-regate-pr) is never subject to it.
+
FOREGROUND_LIVENESS_MAX_DEFER_MS (default 600000, 10
+ minutes), checked every FOREGROUND_LIVENESS_CHECK_INTERVAL_MS (default{" "}
+ 60000, 1 minute — deliberately not the 1-second poll tick, so a job that is
+ still genuinely rate-limited waits for the next sweep instead of busy-looping), releasing at
+ most FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP jobs per tick (default{" "}
+ 25, oldest first, so a large inherited backlog ramps up gradually instead of
+ every released job re-tripping the same rate-limit bucket at once). It also runs once at
+ boot, so a restart self-heals inherited over-deferral. Set{" "}
+ FOREGROUND_LIVENESS_ENABLED=false to disable the sweep.
+
+ OTEL_EXPORTER_OTLP_ENDPOINT overrides the OpenTelemetry collector target only
+ if you're routing to an external collector instead of the bundled one (default{" "}
+ http://otel-collector:4318 under the observability profile).{" "}
+ OTEL_SERVICE_NAME (default gittensory-selfhost) is the service
+ name traces and metrics are tagged with — set a distinct value per instance if you run more
+ than one and want to tell them apart in Grafana/Tempo. OTEL_TRACES_SAMPLER{" "}
+ (default parentbased_traceidratio) picks the sampling strategy for app
+ job/provider traces; pair it with OTEL_TRACES_SAMPLER_ARG (for example{" "}
+ 0.05 to sample 5% of root traces).
+
This table is generated from process.env.NAME reads in{" "}
@@ -199,12 +361,212 @@ features:
rag: false
reputation: false`}
/>
+
+ The features: block above overrides a deployment-wide{" "}
+ GITTENSORY_REVIEW_* flag (rag, reputation, unifiedComment, safety) for this one
+ repo, with three states per key: true forces the capability on for this repo
+ (still subject to the env flag itself being enabled — it can never turn on a capability the
+ operator has fully disabled at the deployment level); false forces it off for
+ this repo regardless of the env flag; and omitting the key entirely falls back to the{" "}
+ GITTENSORY_REVIEW_REPOS allowlist default, i.e. today's behavior for an
+ operator who hasn't set anything here. See{" "}
+ Tuning your reviews for the full{" "}
+ GITTENSORY_REVIEW_* flag list this overrides.
+
+ Everything above has a dashboard row it mirrors. The fields below exist{" "}
+ only in .gittensory.yml — there is no DB column or dashboard
+ toggle for them, so a self-host operator who never reads the example file may not know they
+ exist.
+
+ Controls only whether/how the required Gittensory Orb Review Agent check-run is
+ published — it never affects gate evaluation, comments, labels, audit records, or autonomous
+ merge/close, all of which run identically in every mode. Takes precedence over the legacy{" "}
+ gate.enabled boolean when both are set.
+
disabled, remove Gittensory Orb Review Agent{" "}
+ from this repo's branch-protection or ruleset required-status-checks list — Gittensory
+ cannot do this on your behalf, and leaving it required with nothing to satisfy it means
+ GitHub shows a pending status forever. Keep your real CI/Codecov/security checks required;
+ this setting only ever affects Gittensory's own check-run.
+
+ For a repo that has never been configured, the default is disabled; an
+ already-configured repo keeps its current effective behavior. Self-hosters running
+ high-volume autonomous review should prefer visible or disabled{" "}
+ over required — Gittensory's own merge/close decisions never depend on
+ this check either way.
+
gate.cla — sub-object for the CLA gate (gate.claMode, documented
+ on Tuning your reviews): consentPhrase (a
+ case-insensitive substring gittensory looks for in the PR description),{" "}
+ checkRunName (an existing CLA-bot check-run name that also satisfies
+ consent), and checkRunAppSlug (the trusted App slug required to have produced
+ that check-run, so a contributor-controlled same-name check can't satisfy a blocking
+ legal gate). Either detection method is enough; both may be set. All default to{" "}
+ null (not configured).
+ gate.expectedCiContexts — CI check/status context names to treat as required
+ when GitHub branch protection returns no readable required-status-checks (unconfigured, or
+ a 403 from a token lacking administration:read — common for GitHub App
+ installations, especially self-host). Merged with branch-protection contexts when both are
+ readable; used alone when branch protection is null/empty. Default: not configured, which
+ keeps the fold-all fail-closed behavior when branch protection is also unreadable.
+ gate.premergeContentRecheck — when true, a PR touching{" "}
+ migrations/** gets a fresh GitHub read of the base branch's current
+ migration filenames immediately before an agent-driven merge, catching a different PR that
+ merged a same-numbered migration in the meantime. A live collision holds the PR instead of
+ merging blind. Default false — costs one extra GitHub API call per
+ migrations-touching PR.
+ gate.requireFreshRebaseWindow — when the base branch has advanced within this
+ many minutes of the actual merge decision, forces an update_branch + fresh CI
+ recheck before merging, instead of trusting a possibly-stale{" "}
+ mergeable_state: clean read. A bounded retry cap prevents a fast-moving base
+ from live-locking the PR. Default null (never force).
+ gate.dryRun — when true, the posted check conclusion remains the
+ real non-enforcing verdict while comments/check text may also show the would-be stricter
+ verdict for AI-review blocker mode. It does not disable downstream merge/close planning
+ for failures from already-enforced gates. Default false.
+ gate.firstTimeContributorGrace — reserved and currently inert: parsed and
+ stored, but the gate does not read it. A first-time contributor with a real blocker is
+ one-shot closed the same as a repeat contributor. Kept for potential future use.
+
+ settings.closeOwnerAuthors — by default, the repo owner's own PRs (and{" "}
+ ADMIN_GITHUB_LOGINS fleet-operator PRs) are never auto-closed; they may still
+ auto-merge when clean and passing, or fall to a manual hold. Set true to make
+ owner/admin-authored PRs eligible for auto-close like a contributor's, still gated by
+ the close autonomy class and adverse-signal conditions. Automation-bot PRs stay exempt
+ regardless of this setting. Default false.
+
+ blockedPaths (top-level, alongside wantedPaths) — globs off-limits
+ to contributors. Touching one yields a manifest_blocked_path finding,
+ enforceable when gate.manifestPolicy: block is set. Default []{" "}
+ (nothing blocked).
+
+ A cluster of contributor-abuse guardrails, all config-as-code only, all off/unset by + default: +
+contributorOpenPrCap and{" "}
+ contributorOpenIssueCap bound how many PRs/issues a single non-owner/
+ non-admin/non-bot contributor may have open at once; a contributor's newest item
+ above the cap is closed with a clear reason, their oldest items up to the cap stay open.
+ Both are unset (no cap) by default. contributorCapLabel (default{" "}
+ over-contributor-limit) is the label applied on close — set it to explicit{" "}
+ null to close silently. contributorCapCancelCi cancels in-flight
+ CI runs on a cap-triggered close (requires the actions: write App permission;
+ degrades gracefully without it) and falls back to the{" "}
+ CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT env var when unset.
+ reviewNagPolicy (off/
+ hold/close, default off) throttles a contributor
+ who repeatedly pings @gittensory for review on the same PR/issue, once they
+ exceed reviewNagMaxPings (default 3) within{" "}
+ reviewNagCooldownDays (default 5). reviewNagLabel{" "}
+ (default review-nag-cooldown) is applied alongside the hold/close action.{" "}
+ reviewNagMonitoredMentions extends the same cooldown to specific maintainer
+ logins a contributor keeps tagging directly instead of (or in addition to){" "}
+ @gittensory.
+ autoCloseExemptLogins is a
+ shared, repo-scoped list of logins never throttled or closed by these deterministic
+ mechanisms, on top of the standing owner/admin/bot exemption.{" "}
+ accountAgeThresholdDays (default null, off) applies{" "}
+ newAccountLabel (default new-account) to a PR from a
+ below-threshold-age account — friction/visibility only, never an automatic close on
+ account age alone, and never for the owner, admins, or bots.
+ commandRateLimitPolicy (
+ off/hold, default off) generalizes the review-nag
+ pattern to every @gittensory command, not just review-request pings.{" "}
+ commandRateLimitMaxPerWindow (default 20) bounds cheap,
+ cache-only commands; commandRateLimitAiMaxPerWindow (default 5)
+ is the tighter limit for AI-cost-bearing commands (ask/blockers/preflight/etc.);{" "}
+ commandRateLimitWindowHours (default 24) is the rolling window
+ both limits count against.
+
+ Lets a self-hosted maintainer point Gittensory at their own structured registry (a
+ subnet/plugin/package catalog, for example) without a Gittensory code change — reviewing
+ additions to a data file the same way it reviews code. Unconfigured by default; uncomment
+ and set at least entryFileGlob and collectionField (both required
+ — the whole block is ignored with a warning if either is missing).
+
+ SELFHOST_DEPLOYMENT_MODE forces write suppression for the whole instance,
+ regardless of per-repo autonomy — useful for running a self-host in parallel with the live
+ cloud App on the same webhooks, provably posting nothing until an explicit cutover.
+
ORB_ANONYMIZE=true), not unconditionally — an operator can set{" "}
+ ORB_ANONYMIZE=false to export raw repo/PR names instead. There's no scenario
+ where gittensory's own hosted collector needs raw names; the toggle exists for an operator
+ running their own collector (see ORB_COLLECTOR_URL below) who
+ wants readable identifiers in their own infrastructure. Leave this at the default unless you
+ control the collector end.
+
+ ORB_COLLECTOR_URL overrides the export endpoint — default gittensory's hosted
+ collector, or point it at your own private collector if you're aggregating telemetry
+ yourself instead of sending it to gittensory. ORB_COLLECTOR_TOKEN is the bearer
+ credential for that private collector; leave it unset when using gittensory's own hosted
+ collector, which accepts unauthenticated, rate-limited, aggregate-only exports.
+
+ ORB_APP_ID overrides the seed used to derive this instance's stable,
+ anonymous instance_id in telemetry exports — normally derived from{" "}
+ GITHUB_APP_ID. A brokered instance holds no App ID of its own (it uses the
+ broker's tokens instead), so its identity falls back to the export secret unless you
+ set ORB_APP_ID explicitly. Most operators never need to set this; it exists so
+ a brokered instance's telemetry identity can be pinned independent of any App ID.
+
diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx index 7800038b54..d1b3f3b389 100644 --- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx +++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx @@ -133,6 +133,42 @@ docker compose --profile postgres --profile observability --profile backup up -d watching for.
++ Don't confuse these — they're unrelated features that happen to share the same two chat + platforms: +
+
+ DISCORD_WEBHOOK_URL is a global fallback Discord channel for any repo without
+ its own webhook. DISCORD_REPO_WEBHOOKS is a per-repo override — a JSON map of{" "}
+ owner/repo to a webhook URL — for routing different repos' notifications to
+ different channels. Both are unset (no Discord notifications) by default.
+
+ SLACK_WEBHOOK_URL posts the same per-action events (merged/closed/manual) as a
+ Block Kit section to one Slack channel. Unlike Discord there is no per-repo map today —
+ every repo shares this one webhook. Unset means no Slack notifications.
+
Measured rows below come from a real production instance running the full @@ -420,6 +456,14 @@ volumes: runner-work-2:`} /> +
+ SENTRY_SERVER_NAME sets a clean, human name for this instance in Sentry (for
+ example gittensory-us-east). Unset defaults to the OS hostname — never the
+ public-origin URL. Set this explicitly if you run more than one instance and want to tell
+ their Sentry events apart at a glance instead of matching container hostnames.
+
Leave SENTRY_TRACES_SAMPLE_RATE unset or blank to disable trace export, or set
diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-rag.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-rag.tsx
index 7da7216b19..d3e459a661 100644
--- a/apps/gittensory-ui/src/routes/docs.self-hosting-rag.tsx
+++ b/apps/gittensory-ui/src/routes/docs.self-hosting-rag.tsx
@@ -53,6 +53,17 @@ function SelfHostingRag() {
]}
/>
+
+ SQLite vectors are the default and need no extra service — fine for a small instance or
+ getting started. Qdrant (QDRANT_URL, --profile qdrant) is the
+ preferred dedicated vector store for review context at scale. A third option,{" "}
+ PGVECTOR_ENABLED=true, uses the Postgres pgvector table instead — only relevant
+ if you're already running the postgres profile and want to avoid standing up a
+ separate Qdrant service. Leave it false (the default) when{" "}
+ QDRANT_URL is set; Qdrant remains preferred for RAG at scale.
+
+ AI_EMBED_API_KEY is the bearer credential for AI_EMBED_BASE_URL,
+ if that endpoint requires one — a local Ollama typically doesn't, but a hosted
+ OpenAI-compatible embeddings endpoint usually does. Setting AI_EMBED_MODEL{" "}
+ alone does nothing without AI_EMBED_BASE_URL also set; unset, embeddings use
+ the same provider as the rest of the review chain.
+
diff --git a/apps/gittensory-ui/src/routes/docs.tuning.tsx b/apps/gittensory-ui/src/routes/docs.tuning.tsx index 98608f20d4..79c3f92f8e 100644 --- a/apps/gittensory-ui/src/routes/docs.tuning.tsx +++ b/apps/gittensory-ui/src/routes/docs.tuning.tsx @@ -1,4 +1,4 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, Link } from "@tanstack/react-router"; import { DocsPage } from "@/components/site/docs-page"; import { CodeBlock, Callout } from "@/components/site/primitives"; @@ -58,6 +58,13 @@ function Tuning() { algorithm and never reveal review direction, so a contributor cannot read them and game the gate.
++ This page covers those fields in depth, for the cloud service or a self-host alike. If + you're running your own instance, see{" "} + Self-host configuration for the + environment layer (deployment-wide flags, secrets, and where config files can live) that + sits underneath everything below. +
For the privacy guarantees behind these surfaces, see{" "}
Privacy & security. For the maintainer install and
- trust flow, see Install & trust.
+ trust flow, see Install & trust. If you're
+ self-hosting, see Self-host configuration{" "}
+ for the environment layer these settings sit on top of, plus the config-precedence rules and
+ a link to the fully-commented .gittensory.yml.example.