-
-
Notifications
You must be signed in to change notification settings - Fork 71
Expand file tree
/
Copy path.env.example
More file actions
785 lines (747 loc) · 67.3 KB
/
Copy path.env.example
File metadata and controls
785 lines (747 loc) · 67.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
# Gittensory — sample environment file
#
# Copy to `.dev.vars` (local) or set as worker vars/secrets (deployed). This file
# lists every operator-facing LOOPOVER_REVIEW_* feature flag plus the names of
# the secrets the worker reads. It contains NO real values — fill secrets in via
# `wrangler secret put NAME`; never commit real secret values.
#
# Feature flags: every flag defaults OFF. "Truthy" = one of 1 / true / yes / on
# (case-insensitive); unset, empty, or `false` is OFF. When a flag is OFF its
# code path is fully inert — the review behaves as if the feature did not exist.
#
# See https://loopover.ai/docs/tuning for the full reference
# (flags, per-repo `.loopover.yml` settings, and secret descriptions).
# =============================================================================
# 1. Review feature flags (LOOPOVER_REVIEW_*)
# =============================================================================
# --- Scope (per-repo cutover allowlist) -------------------------------------
# Comma-separated owner/repo names allowed to run the per-PR review features
# (SAFETY, GROUNDING, RAG, REPUTATION). A per-PR feature runs on
# a repo only if its own flag is ON *and* the repo is listed here. Empty = no
# repos, so every per-PR feature stays dormant regardless of the flags below.
# Case-insensitive, trimmed; stray commas ignored.
# Example: LOOPOVER_REVIEW_REPOS="JSONbored/gittensory,JSONbored/awesome-claude"
LOOPOVER_REVIEW_REPOS=
# GITTENSORY_REVIEW_REPOS= # no longer read (removed by #4777)
# Instance-wide write kill switch for the cloud→self-host parallel-run migration. When set to "dry-run"
# (or "disabled"), EVERY GitHub write from this instance is suppressed regardless of per-repo settings —
# the instance can receive webhooks and compute verdicts but posts NOTHING (no check-run/comment/label/
# merge), so it can shadow the live cloud App safely until cutover. "dry-run" audits as completed-shadow;
# "disabled" audits as denied. Leave empty (= live) for normal operation. Flip to live only at cutover.
# SELFHOST_DEPLOYMENT_MODE=dry-run
# --- Per-PR capabilities (also require the repo in LOOPOVER_REVIEW_REPOS) ---
# Safety scan: defangs untrusted PR title/body/diff (prompt-injection
# neutralization) and scans the diff for leaked secrets (secret_leak blocker).
LOOPOVER_REVIEW_SAFETY=false
# GITTENSORY_REVIEW_SAFETY=false # no longer read (removed by #4777)
# Grounds the AI-reviewer prompt with the PR's finished CI status + the full
# post-change content of the changed files, so claims are verified against reality.
LOOPOVER_REVIEW_GROUNDING=false
# GITTENSORY_REVIEW_GROUNDING=false # no longer read (removed by #4777)
# Retrieval-augmented context: appends semantically related code/docs from the
# self-host vector index to the reviewer prompt. Inert until Qdrant or sqlite vectors are populated.
LOOPOVER_REVIEW_RAG=false
# GITTENSORY_REVIEW_RAG=false # no longer read (removed by #4777)
# Review-enrichment service (REES): POSTs the PR diff/files to the external
# enrichment service and splices any public-safe brief into the AI reviewer
# prompt. Also requires REES_URL and the repo in LOOPOVER_REVIEW_REPOS.
LOOPOVER_REVIEW_ENRICHMENT=false
# GITTENSORY_REVIEW_ENRICHMENT=false # no longer read (removed by #4777)
# REES_URL=https://enrichment.example.internal
# REES_SHARED_SECRET= # bearer secret configured on the REES service
# REES_TIMEOUT_MS=8000 # optional; minimum 1000, default 8000
# REES_PROFILE=balanced # optional; fast | balanced | deep. Unset uses balanced.
# REES_FORWARD_GITHUB_TOKEN=false # optional; default false. Set true only when REES_URL is inside
# # your trust boundary and token-aware analyzers need a GitHub
# # token for CODEOWNERS/blob-size reads (installation token when
# # available; otherwise GITHUB_PUBLIC_TOKEN).
# REES_ANALYZERS=all # all | comma-list of exact names.
# # Unknown names warn and are ignored; a typo-only list runs no analyzers.
# BEGIN GENERATED REES ANALYZERS
# Current analyzer names:
# dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,duplicationDelta
# churnHotspot,blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch
# commitHygiene,pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology
# todoMarker,magicNumber,conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting
# errorSwallow,complexity,complexityDelta,unsafeAny,a11y,i18n,unusedExport,exhaustiveness
# flakyTest,commitLint,apiBreak,deprecatedDep,revertRecurrence,coverageDelta,callerImpact
#
# Profile defaults:
# fast: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,secretLog,typosquat,iacMisconfig,nativeBuild
# testRatio,migrationSafety,looseRange,terminology,todoMarker,magicNumber,conflictMarker
# debugLeftover,sizeSmell,floatingPromise,deepNesting,errorSwallow,complexity,unsafeAny,a11y
# i18n,apiBreak,deprecatedDep
# balanced (default): dependency,dependencyDiff,lockfileDrift,secret,license,installScript
# heavyDependency,hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight
# typosquat,commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication
# duplicationDelta,churnHotspot,blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport
# staleBranch,commitHygiene,pendingReviewRequests,testRatio,migrationSafety,looseRange
# terminology,todoMarker,magicNumber,conflictMarker,debugLeftover,sizeSmell,floatingPromise
# deepNesting,errorSwallow,complexity,complexityDelta,unsafeAny,a11y,i18n,unusedExport
# exhaustiveness,flakyTest,commitLint,apiBreak,deprecatedDep,revertRecurrence,coverageDelta
# callerImpact
# deep: dependency,dependencyDiff,lockfileDrift,secret,license,installScript,heavyDependency
# hardcodedUrl,actionPin,eol,redos,provenance,codeowners,secretLog,assetWeight,typosquat
# commitSignature,iacMisconfig,nativeBuild,history,docCommentDrift,duplication,duplicationDelta
# churnHotspot,blameLink,approvalIntegrity,ciCheckSignals,undocumentedExport,staleBranch
# commitHygiene,pendingReviewRequests,testRatio,migrationSafety,looseRange,terminology
# todoMarker,magicNumber,conflictMarker,debugLeftover,sizeSmell,floatingPromise,deepNesting
# errorSwallow,complexity,complexityDelta,unsafeAny,a11y,i18n,unusedExport,exhaustiveness
# flakyTest,commitLint,apiBreak,deprecatedDep,revertRecurrence,coverageDelta,callerImpact
# END GENERATED REES ANALYZERS
# Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep
# submitters to a deterministic-only review. Never surfaced publicly.
LOOPOVER_REVIEW_REPUTATION=false
# GITTENSORY_REVIEW_REPUTATION=false # no longer read (removed by #4777)
# --- Global capabilities (NOT scoped by LOOPOVER_REVIEW_REPOS) -------------
# Observability (read-only): cron anomaly scan over the gate-block ledger emits
# ops_anomaly logs, plus a bearer-gated GET /v1/internal/ops/stats aggregate.
LOOPOVER_REVIEW_OPS=false
# GITTENSORY_REVIEW_OPS=false # no longer read (removed by #4777)
# Self-improvement / auto-tune loop: computes tuning recommendations, shadow-soaks
# strictly-tightening ones, and auto-promotes only after the soak passes. Tightening-only.
LOOPOVER_REVIEW_SELFTUNE=false
# GITTENSORY_REVIEW_SELFTUNE=false # no longer read (removed by #4777)
# Parity readiness (shadow, record-only): shadow-records each finalized gate
# decision and serves a readiness report at GET /v1/internal/parity. Changes no behavior.
LOOPOVER_REVIEW_PARITY_AUDIT=false
# GITTENSORY_REVIEW_PARITY_AUDIT=false # no longer read (removed by #4777)
# Content-review lane: routes content repos (curated lists, registries) through the
# dedicated content lane (dedup, source-evidence, scope, registry grounding).
LOOPOVER_REVIEW_CONTENT_LANE=false
# GITTENSORY_REVIEW_CONTENT_LANE=false # no longer read (removed by #4777)
# Public draft-submission flow: enables the /v1/drafts endpoints (contributor draft
# -> GitHub OAuth -> fork PR). OFF every draft endpoint 404s. Needs draft secrets below.
LOOPOVER_REVIEW_DRAFT=false
# GITTENSORY_REVIEW_DRAFT=false # no longer read (removed by #4777)
# =============================================================================
# 2. Secrets (names only — set with `wrangler secret put NAME`)
# =============================================================================
# Do NOT put real values here. Set each via `wrangler secret put NAME` (deployed)
# or in `.dev.vars` (local, git-ignored). Listed by name so operators know what
# the worker reads.
# --- Core (required for the worker to run) -----------------------------------
# GITHUB_WEBHOOK_SECRET=
# GITHUB_APP_ID=
# GITHUB_APP_PRIVATE_KEY=
# GITHUB_APP_SLUG=
# GITTENSOR_REGISTRY_URL=
# LOOPOVER_API_TOKEN= # server-to-server API bearer token
# GITTENSORY_API_TOKEN= # no longer read (removed by #4777)
# LOOPOVER_MCP_TOKEN= # shared MCP bearer token
# GITTENSORY_MCP_TOKEN= # no longer read (removed by #4777)
# INTERNAL_JOB_TOKEN=
# --- Optional (capability-gated; degrade safely when absent) -----------------
# GITHUB_OAUTH_CLIENT_ID= # GitHub OAuth (dashboard sign-in, draft flow)
# GITHUB_OAUTH_CLIENT_SECRET= # GitHub OAuth; also required by the draft flow
# GITHUB_PUBLIC_TOKEN= # unauthenticated public-GitHub reads (.loopover.yml fetch)
# TOKEN_ENCRYPTION_SECRET= # AES-256-GCM master secret for maintainer BYOK keys at rest
# DRAFT_TOKEN_ENCRYPTION_SECRET= # AES-256-GCM secret for the contributor OAuth token (draft flow)
# LOOPOVER_REVIEW_STATS_TOKEN= # bearer token guarding the stats data endpoint
# LOOPOVER_DRIFT_ISSUE_TOKEN= # token for auto-filing drift issues
# LOOPOVER_CONTRIBUTOR_ISSUE_TOKEN= # token for contributor-issue automation
# PRODUCT_USAGE_HASH_SALT= # salt for hashing product-usage identifiers
# =============================================================================
# 3. Self-host (Docker) — runtime config (#980)
# =============================================================================
# For `docker compose up` self-hosting (NOT the Cloudflare Worker deploy). Copy this file to `.env`
# (gitignored), UNCOMMENT + fill the required Core secrets in section 2, then add the runtime values below.
# Every value here is a SAMPLE placeholder — never commit real secrets.
#
# RECOMMENDED over pasting secret values into this file at all: docker-compose.yml's native `secrets:`
# mounts (secrets/README.md) cover the Core secrets above plus TOKEN_ENCRYPTION_SECRET,
# DRAFT_TOKEN_ENCRYPTION_SECRET, SELFHOST_SETUP_TOKEN, ORB_ENROLLMENT_SECRET, PAGERDUTY_ROUTING_KEY,
# and CLAUDE_CODE_OAUTH_TOKEN.
# Run `./scripts/selfhost-init-secrets.sh` once, then write each real value into its file under secrets/
# instead of uncommenting the var here — an inline .env value always takes priority if you set both, so
# migrating is safe to do one secret at a time.
# PUBLIC_API_ORIGIN=https://reviews.example.com # REQUIRED before the first-run setup wizards (GET /setup and
# # GET /orb/setup). The wizard embeds this origin in the GitHub App
# # manifest's redirect_url; without it the wizard returns 400. Use the
# # EXACT public URL operators browse to (scheme + host [+ port]).
# # Deriving it from the request Host header would let an attacker
# # redirect the App-creation callback, so it must be set explicitly.
# # Stays load-bearing after setup too: this same origin is embedded
# # as the <img src> for every visual-capture screenshot posted to a
# # PR comment (see PUBLIC_SITE_ORIGIN below), so it must be
# # reachable from GitHub's own servers, not just this host or your
# # tailnet. See PUBLIC_ORIGIN_ACKNOWLEDGED below.
# PUBLIC_SITE_ORIGIN=https://your-fork.example.com # "before" screenshots in the visual-capture PR comment
# # table render THIS origin's production pages (vs. PUBLIC_API_ORIGIN,
# # which serves the screenshot images themselves). Unset ⇒ every
# # "Before (production)" cell is blank, not an error — visual capture
# # degrades to after-only, never fails the review.
# PUBLIC_ORIGIN_ACKNOWLEDGED=false # silences the boot-time warning when PUBLIC_API_ORIGIN/
# # PUBLIC_SITE_ORIGIN look like a private/internal hostname (e.g. a
# # bare Tailscale MagicDNS `*.ts.net` address) once you've confirmed
# # it's genuinely public (Funnel enabled, a reverse proxy in front of
# # it, etc.). Default false (warning shown). If your instance is
# # NOT genuinely public and you'd rather not expose it, see
# # REVIEW_AUDIT_S3_BUCKET below instead -- it moves screenshot
# # serving to a public bucket so this warning never applies to
# # PUBLIC_API_ORIGIN in the first place.
# SELFHOST_SETUP_TOKEN=change-this-long-random-value # REQUIRED to unlock the first-run /setup wizard. Without it
# # /setup returns 400; with it, enter the token in the browser form
# # or send an x-setup-token / Bearer header. Never put this token in
# # the URL; query strings leak to logs, proxies, and browser history.
# SETUP_OUTPUT_PATH=/data/gittensory-app.env # overrides where the first-run setup wizard writes the generated
# # GitHub App credentials file. Default: /data/gittensory-app.env.
# ADMIN_GITHUB_LOGINS=your-github-login # REQUIRED for control-panel ("operator") access. A comma- or
# # whitespace-separated allowlist of GitHub logins, case-insensitive.
# # FAIL-CLOSED: unset/empty means NOBODY can sign into the dashboard,
# # even the person who set up this instance — this is not a bug, add
# # your own login here right after first-run setup. Also exempts these
# # logins from the agent's own-PR auto-close rules (fleet-operator
# # identity) and lets them bypass per-repo MCP scope.
# CONTRIBUTOR_CAP_CANCEL_CI_DEFAULT=false # install-wide default for the per-repo contributorCapCancelCi
# # setting: cancel in-flight CI runs when a PR is auto-closed for
# # exceeding contributorOpenPrCap. A repo's own configured value
# # always takes precedence. Requires the App installation to have
# # granted the actions:write permission -- degrades gracefully
# # (skipped, logged) when it hasn't. Off by default.
# MCP_READ_REPO_ALLOWLIST= # scopes the shared LOOPOVER_MCP_TOKEN identity's READ-only MCP
# # tools (repo context, issue quality, watch subscriptions) to these
# # owner/repo entries (comma/whitespace-separated). FAIL-CLOSED:
# # unset/empty grants no repo access. `*` or `all` is an explicit
# # escape hatch for full unscoped read access, incl. cross-repo
# # contributor/operator tools that have no single repo to scope against.
# MCP_ACTUATION_REPO_ALLOWLIST= # same fail-closed/wildcard model as MCP_READ_REPO_ALLOWLIST, but for
# # WRITE (merge/close/approve) MCP tools — kept separate so read and
# # write trust can differ. Unrelated to ADMIN_GITHUB_LOGINS, which
# # scopes dashboard sign-in, not the shared MCP token's per-repo reach.
# PORT=8787
# DATABASE_PATH=/data/loopover.sqlite # SQLite file on the mounted data volume; all migrations auto-apply
# LOOPOVER_REPO_CONFIG_DIR=/config # in-container dir the app reads per-repo .loopover.yml files from
# CONFIG_DIR_EMPTY_ACKNOWLEDGED=true # silences the boot-time warning when LOOPOVER_REPO_CONFIG_DIR is set
# # but the mounted directory is empty (every per-repo/global setting
# # silently falls back to built-in defaults). Set once you've
# # confirmed this is intentional, e.g. a fresh install with no
# # .loopover.yml written yet. Default false (warning shown).
# COMPOSE_PROJECT_NAME=loopover # Docker Compose's own project name; also labels the log stream
# # Promtail ships to Loki. Change it to run two stacks on one host.
# TZ=UTC # timezone passed to n8n (--profile workflows) for cron schedules
# POSTGRES_PASSWORD=change-this-long-random-value # used by the --profile postgres / --profile pgbouncer services
# DATABASE_URL= # set to postgres://user:pw@host:5432/db to use Postgres instead of
# # SQLite (shared DB → multi-instance). Overrides DATABASE_PATH.
# # Compose examples:
# # postgres://loopover:<POSTGRES_PASSWORD>@postgres:5432/loopover
# # postgres://loopover:<POSTGRES_PASSWORD>@pgbouncer:5432/loopover
# PGVECTOR_ENABLED=false # set true only when using the Postgres pgvector table for RAG.
# # Leave false when QDRANT_URL is set; Qdrant remains the preferred
# # dedicated vector store for review context at scale.
# PGPOOL_MAX=10 # max concurrent Postgres connections in the pool shared by every
# # HTTP handler AND every queue worker's own DB traffic. 10 (this
# # default) is fine for a small/idle instance; raise it if you
# # register many repos or run higher QUEUE_CONCURRENCY, and you see
# # request/job latency climb without Postgres itself under pressure
# # (check GittensoryPostgresConnectionPressure and the app's own
# # request-latency metrics to tell those two cases apart).
REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review runtime. The default compose stack
# # starts Redis automatically; override for an external Redis.
# LOOPOVER_IMAGE=ghcr.io/jsonbored/loopover-selfhost:latest # image used by scripts/deploy-selfhost-image.sh;
# # pin production rollouts to a release tag such as :orb-v0.1.0
# # or to an immutable @sha256 digest.
# GITHUB_CACHE_TTL_SECONDS=20 # Enables the Redis-backed GitHub GET-response cache when >0; set 0
# # to disable. NOT a per-entry TTL duration — each cached class
# # resolves its own TTL below (GITHUB_BRANCH_PROTECTION_CACHE_TTL_SECONDS
# # etc.); the numeric value here is otherwise unused.
# GITHUB_BRANCH_PROTECTION_CACHE_TTL_SECONDS=1200 # TTL for required-status branch protection reads.
# GITHUB_METADATA_CACHE_TTL_SECONDS=600 # TTL for stable repo/user/installation metadata reads.
# GITHUB_COMMIT_CACHE_TTL_SECONDS=900 # TTL for bare /commits/{ref} resolves; dedups the two hourly upstream ref→SHA reads.
# GITHUB_WEBHOOK_MAX_BODY_BYTES=1048576 # max accepted GitHub webhook body size, in bytes (default 1 MiB).
# # Larger deliveries are rejected before parsing. Raise only if you
# # see legitimate webhooks rejected for size (e.g. a `push` event
# # with a large `commits` array from a big force-push or merge);
# # most installs never need to touch this.
# QDRANT_URL= # set to http://qdrant:6333 to use Qdrant as the RAG vector store
# # (--profile qdrant). Overrides the built-in sqlite-vec / pgvector.
# BROWSER_WS_ENDPOINT= # ws:// URL of a browserless/chrome-compatible instance for
# # visual-review screenshot capture. Unset = visual review is
# # fully inert (no screenshots, no error) — entirely optional.
# # Bundled option (--profile visual-review): set this to
# # ws://browserless:3000?token=${BROWSERLESS_TOKEN} using the
# # SAME value as BROWSERLESS_TOKEN below. Or point at your own
# # externally-run browserless/chromium instance instead.
# BROWSERLESS_TOKEN= # auth token for the bundled --profile visual-review browserless
# # container. Set a strong random value and embed the SAME
# # value in BROWSER_WS_ENDPOINT's ?token= above. Unset ⇒ the
# # container auto-generates a random token each boot that will
# # never match a hardcoded BROWSER_WS_ENDPOINT — set this.
# # Irrelevant if you point BROWSER_WS_ENDPOINT at an external
# # browserless instance you manage yourself.
# BROWSERLESS_CONCURRENT=2 # (--profile visual-review) max simultaneous Chromium sessions.
# BROWSERLESS_QUEUED=4 # (--profile visual-review) max requests waiting for a session.
# BROWSERLESS_TIMEOUT_MS=30000 # (--profile visual-review) per-session timeout.
# BROWSERLESS_MEM_LIMIT=2g # (--profile visual-review) container memory cap.
# BROWSERLESS_EXPORTER_MEM_LIMIT=64m # (--profile visual-review) browserless-exporter sidecar cap (#5368)
# REVIEW_AUDIT_DIR= # persist visual-review screenshot PNGs to this filesystem path
# # instead of re-rendering on demand. Unset = re-render each time.
# # Only relevant when BROWSER_WS_ENDPOINT above is set. Ignored
# # when REVIEW_AUDIT_S3_BUCKET below is configured (S3 wins).
# --- Visual-review screenshot storage in an S3-compatible bucket (optional; an operator's own Cloudflare R2
# bucket, or any other S3-compatible provider) instead of the local filesystem above. Recommended once
# LOOPOVER_REVIEW_SCREENSHOTS is on: screenshots embedded in a PUBLIC GitHub PR comment need to be
# reachable by GitHub itself and by anyone viewing the PR, not just by this instance's own network. All
# four of the following must be set together to enable it. ---
# REVIEW_AUDIT_S3_BUCKET= # bucket name.
# REVIEW_AUDIT_S3_ENDPOINT= # e.g. https://<account_id>.r2.cloudflarestorage.com for R2.
# REVIEW_AUDIT_S3_ACCESS_KEY_ID= # e.g. an R2 API token's Access Key ID (Object Read & Write,
# # scoped to this one bucket, is enough).
# REVIEW_AUDIT_S3_SECRET_ACCESS_KEY= # the matching Secret Access Key.
# REVIEW_AUDIT_S3_REGION=auto # "auto" for R2 (its own S3-API convention); a real AWS region
# # string for other S3-compatible providers. Default "auto".
# REVIEW_AUDIT_S3_PUBLIC_URL= # the bucket's own public base URL (an R2 public r2.dev bucket
# # URL, or a custom domain connected to the bucket) -- when set,
# # screenshots link DIRECTLY at this URL instead of proxying
# # through this instance's own /gittensory/shot route. Strongly
# # recommended alongside the bucket vars above; without it,
# # screenshots are stored in the bucket but still served through
# # this instance (same reachability requirement as REVIEW_AUDIT_DIR).
# DISCORD_WEBHOOK_URL= # one Discord channel for per-action notifications (merged/closed/
# # manual) on ANY repo you review. Unset = no Discord notifications.
# # Collection and schema are auto-created at startup. Off when unset.
# QDRANT_API_KEY= # Bearer token for an authenticated Qdrant (cloud / on-prem). Omit for
# # the local --profile qdrant container (unauthenticated).
# QDRANT_DIM=1024 # vector dimension of the collection (1024 = bge-m3/mxbai-embed-large,
# # the recommended default -- bge-m3's longer native context window
# # is a better match for RAG's ~16k-char chunk budget than smaller
# # embedders; 768 = nomic-embed-text:latest, a lighter alternative).
# # Must match AI_EMBED_MODEL; recreate the Qdrant collection when
# # changing this after startup (existing vectors are a fixed width
# # and cannot be resized in place).
# AI_EMBED_BATCH=96 # items per RAG embed-provider call (#4327). Defaults to 96
# # (a conservative bound sized for Workers AI's 100-item cap).
# # Tune upward for GPU-accelerated self-host Ollama throughput --
# # benchmarking on an RTX A5000 found 96 already near-optimal
# # (~34ms/chunk vs ~70ms/chunk at 32), so leave unset unless you
# # have hardware-specific data suggesting otherwise.
# LOOPOVER_REPORTING_SOURCE_DATABASE_URL= # optional Postgres reporting reader URL. Defaults to DATABASE_URL.
# LOOPOVER_BACKUP_SOURCE_DATABASE_URL= # optional Postgres backup reader URL. Defaults to DATABASE_URL.
# MIGRATIONS_DIR=/app/migrations
# CRON_INTERVAL_MS=120000 # maintain/sweep + sync cadence (default ~2 min)
# --- Per-service memory limits (docker-compose.yml; #1828/#2495) ---
# Every service below gets a beginner-friendly default `deploy.resources.limits.memory` cap so one runaway
# optional profile (e.g. Ollama loading a multi-GB model) can't OOM-kill or starve the core review pipeline.
# Override any of these for a bigger/smaller host; values use Docker's byte-size shorthand (e.g. 512m, 4g).
# LOOPOVER_MEM_LIMIT=2g # core app (always runs)
# REDIS_MEM_LIMIT=512m # core app (always runs)
# POSTGRES_MEM_LIMIT=2g # --profile postgres / --profile pgbouncer
# QDRANT_MEM_LIMIT=2g # --profile qdrant
# REES_MEM_LIMIT=512m # --profile rees (the review-enrichment service)
# OLLAMA_MEM_LIMIT=20g # --profile ollama; raise this before pulling a large local model
# # (default sized for an embed model + one vision model resident
# # together, #4335 -- raise further for additional/larger models)
# PROMETHEUS_MEM_LIMIT=1g # --profile observability
# LOKI_MEM_LIMIT=1g # --profile observability
# TEMPO_MEM_LIMIT=1g # --profile observability
# GRAFANA_MEM_LIMIT=512m # --profile observability
# ALERTMANAGER_MEM_LIMIT=256m # --profile observability
# DOCKER_PROXY_MEM_LIMIT=64m # --profile observability
# PROMTAIL_MEM_LIMIT=256m # --profile observability
# OTEL_COLLECTOR_MEM_LIMIT=512m # --profile observability
# NODE_EXPORTER_MEM_LIMIT=128m # --profile observability (#5366)
# CADVISOR_MEM_LIMIT=256m # --profile observability (#5366)
# REDIS_EXPORTER_MEM_LIMIT=64m # --profile observability (#5366)
# --- Continuous backup (optional; the Litestream sidecar in docker-compose.yml) ---
# Blank is valid until --profile litestream is enabled.
# LITESTREAM_ACCESS_KEY_ID=
# LITESTREAM_SECRET_ACCESS_KEY=
# LITESTREAM_ENDPOINT= # e.g. s3.us-west-002.backblazeb2.com (omit for AWS S3)
# # With --profile storage, point at local MinIO instead:
# # LITESTREAM_ENDPOINT=http://minio:9000
# # LITESTREAM_ACCESS_KEY_ID=${MINIO_ROOT_USER}
# # LITESTREAM_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
# LITESTREAM_REGION=us-east-1
# BACKUP_ACKNOWLEDGED=false # silences the boot-time warning about running SQLite with no
# acknowledged backup once you've wired Litestream (or an
# equivalent) above. Default false (warning shown).
# --- Snapshot backups (--profile backup; the backup + backup-exporter sidecars) ---
# Separate from Litestream above: this runs scripts/backup.sh on a loop and exports the result as metrics.
# BACKUP_RETAIN=7 # how many snapshots to keep before pruning the oldest
# BACKUP_INTERVAL_SECONDS=86400 # seconds between runs (default 86400 = daily)
# Opt-in restore drill (scripts/verify-backup.sh): both are empty by default, which skips the drill. Set the
# pair together to restore each snapshot into a scratch database and prove it is actually restorable.
# VERIFY_RESTORE_SCRATCH= # non-empty enables the drill
# LOOPOVER_VERIFY_SCRATCH_DATABASE_URL= # throwaway scratch DB the drill restores into (never your live DB)
# --- n8n workflow automation (--profile workflows) ---
# N8N_PASSWORD=changeme # REQUIRED at runtime when using --profile workflows
# N8N_USER=admin
# N8N_WEBHOOK_URL=https://n8n.example.com # set to public URL if receiving external webhooks
# N8N_ENCRYPTION_KEY= # 32-char random string; persists credentials across restarts
# N8N_MEM_LIMIT=512m
# --- MinIO S3-compatible object storage (--profile storage) ---
# MINIO_ROOT_USER=minio # REQUIRED when using --profile storage
# MINIO_ROOT_PASSWORD=changeme # REQUIRED when using --profile storage
# MINIO_BUCKET=gittensory # default bucket name (create via console at http://localhost:9001)
# MINIO_MEM_LIMIT=1g
# Console at http://localhost:9001
# S3 API at http://minio:9000 (from other containers) or http://localhost:9000 (from host)
# --- Queue worker (#977/#1201) ---
# QUEUE_CONCURRENCY=4 # max concurrent job-processing loops per instance. Default 4; set 1
# # for strict serial processing. 8 is a reasonable starting point for
# # a moderate-load instance (several active repos / a steady
# # contributor-PR stream) on a host with a few spare cores -- review
# # jobs are I/O-bound (GitHub + AI awaits dominate), so raising this
# # mostly buys parallelism, not CPU. As a core-count anchor (#1828):
# # a 2-vCPU host is comfortable around the default of 4; a 4+ vCPU
# # host can reasonably go to 8-12 before the loops' own CPU work
# # (JSON parsing, diff rendering, AI-response handling) starts
# # competing for the cycles the I/O awaits were supposed to free up.
# # Watch loopover_queue_live_pending
# # / loopover_queue_oldest_live_pending_age_seconds after raising it;
# # if those stay high, the bottleneck is elsewhere (GitHub rate limit,
# # AI latency, Postgres pool -- see PGPOOL_MAX above), not concurrency.
# QUEUE_BACKGROUND_CONCURRENCY=1 # max low-priority/background jobs allowed to occupy QUEUE_CONCURRENCY slots
# # (default 1 reserves the rest of even a small 2-vCPU host's default
# # 4 slots for live review work). Scale it WITH QUEUE_CONCURRENCY, not
# # independently -- keep it to roughly a quarter to a third of
# # QUEUE_CONCURRENCY (e.g. 2-3 of a 4+ vCPU host's 8-12) so background
# # maintenance can never crowd out every live-review slot.
# CONTRIBUTOR_EVIDENCE_BATCH_SIZE=150 # logins per build-contributor-evidence job; the scheduled run fans out into
# # per-batch jobs above this so the per-login GitHub reads spread across the
# # queue instead of bursting. Set 0 to disable the fan-out (single job).
# --- Maintenance-job backpressure (#selfhost-runtime-pressure) ---
# Periodic maintenance sweeps (contributor evidence, burden forecasts, RAG re-indexing, drift scans, product
# rollups, notifications...) already yield to an exhausted GitHub REST budget; these knobs add an orthogonal
# check at queue-claim time -- is the BOX itself under load right now -- so maintenance never competes with
# live webhook/review work for CPU/DB time. A denied maintenance job is deferred with jitter, never dropped;
# TRICKLE (MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS) force-admits it once it's waited long enough regardless of
# pressure, so sustained load can slow maintenance down but never starve it forever. All defaults are sane for
# a small single-node box; every value is optional.
# DRAIN (MAINTENANCE_ADMISSION_DRAIN_AGE_MS, #selfhost-maintenance-self-pin): a second, much shorter age escape
# scoped ONLY to `maintenance_pending_high` -- the aggregate lane-backlog check has no feedback loop back to the
# count as jobs individually age out via the trickle above, so without this a backed-up lane can deny every
# claim for hours even though the trickle exists. The drain lets the OLDEST jobs in that same backlog through
# well before the full trickle ceiling, so the backlog can actually shrink (further bounded by
# QUEUE_BACKGROUND_CONCURRENCY). Still fully blocked by host_load_high -- draining more work onto an overloaded
# box is exactly what that check exists to prevent.
# MAINTENANCE_ADMISSION_ENABLED=true # set false/0/off to fully disable this policy (old always-run behavior)
# MAINTENANCE_ADMISSION_MAX_LIVE_PENDING=5 # defer maintenance once this many live (webhook/regate) jobs are queued
# MAINTENANCE_ADMISSION_MAX_LIVE_AGE_MS=120000 # defer maintenance once the oldest live job has waited this long (2m)
# MAINTENANCE_ADMISSION_MAX_PENDING=15 # defer NEW maintenance admissions once this many maintenance jobs are already queued
# MAINTENANCE_ADMISSION_MAX_HOST_LOAD=1.5 # defer once 1-min load average per CPU core exceeds this (best-effort; see host-pressure.ts)
# MAINTENANCE_ADMISSION_MAX_BACKLOG_CONVERGENCE_PENDING=10 # defer maintenance once this many backlog-convergence-tagged PR re-reviews (queue-fairness.ts) are queued -- a real, unresolved PR-review backlog
# MAINTENANCE_ADMISSION_DEFER_MS=180000 # base defer duration on denial, before jitter (3m)
# MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS=14400000 # trickle ceiling: force-admit a maintenance job that has waited this long (4h)
# MAINTENANCE_ADMISSION_DRAIN_AGE_MS=600000 # drain ceiling: admit a job despite a backed-up lane once it has waited this long (10m); clamped to MAINTENANCE_ADMISSION_MAX_DEFER_AGE_MS
# --- Foreground queue liveness (#selfhost-queue-liveness) ---
# Live contributor-PR-review work (github-webhook, agent-regate-pr, agent-regate-sweep, recapture-preview) must
# always have a BOUNDED runnable trickle. Unlike maintenance jobs (which get an admission trickle floor of their
# own, see above), foreground jobs are never exempted from GitHub rate-limit deferral -- its worst case is
# unbounded (up to ~65 minutes per defer, re-triggered indefinitely under sustained pressure, e.g. right after a
# deploy floods a shared REST budget). This sweep periodically pulls
# back any foreground-priority pending job that has been genuinely waiting (since its own enqueue time, not reset
# by a re-defer) past the ceiling below, regardless of which mechanism deferred it -- mirroring the maintenance
# trickle's own escape hatch. Also runs once at boot, so a restart self-heals inherited over-deferral instead of
# needing manual intervention. All defaults are sane; every value is optional.
# FOREGROUND_LIVENESS_ENABLED=true # set false/0/off to fully disable this sweep
# FOREGROUND_LIVENESS_MAX_DEFER_MS=600000 # trickle ceiling: force-release a foreground job deferred this long (10m)
# FOREGROUND_LIVENESS_CHECK_INTERVAL_MS=60000 # sweep cadence -- deliberately NOT the 1s poll tick, so a job
# # still genuinely rate-limited waits for the next sweep instead
# # of busy-looping (1m)
# FOREGROUND_LIVENESS_MAX_RELEASE_PER_SWEEP=25 # ramp-up cap: ceiling on how many foreground jobs one sweep
# # tick releases (oldest first), so a large inherited backlog
# # drains gradually over several ticks instead of every
# # released job re-attempting GitHub at once and immediately
# # re-tripping the same rate-limit bucket it was deferred for
# --- Per-installation GitHub-fetch concurrency (#selfhost-installation-concurrency) ---
# QUEUE_BACKGROUND_CONCURRENCY caps how many background jobs run AT ALL, globally -- it has no notion of WHICH
# installation those jobs belong to, so raising it above the default of 1 lets one installation's background
# sweep/backfill claim every available background slot at once, starving every OTHER installation's background
# work even though neither is anywhere near GitHub-rate-limit exhaustion. This is a THIRD, per-installation
# claim-time admission check (alongside GitHub rate-limit admission and maintenance-lane admission above),
# checked only for background jobs that make GitHub calls -- never for foreground live-PR work
# (github-webhook/agent-regate-pr). A denied job is deferred with jitter, never dropped. Per instance, per
# process (same single-process-per-deployment model as QUEUE_CONCURRENCY/QUEUE_BACKGROUND_CONCURRENCY above).
# GITHUB_INSTALLATION_CONCURRENCY_ENABLED=true # set false/0/off to fully disable this check
# GITHUB_INSTALLATION_CONCURRENCY_LIMIT=2 # max concurrent GitHub-fetching background jobs per installation
# GITHUB_INSTALLATION_CONCURRENCY_DEFER_MS=15000 # base defer duration on denial, before jitter (15s)
# --- Caddy HTTPS terminator (#1203; requires --profile caddy) ---
# DOMAIN=gittensory.example.com # fully-qualified domain; Caddy auto-obtains a Let's Encrypt cert
# --- Tailscale sidecar (#1204; requires --profile tailscale) ---
# Blank is valid until --profile tailscale is enabled.
# TS_AUTHKEY= # Tailscale auth key (generate at tailscale.com/admin/settings/keys)
# TS_EXTRA_ARGS= # extra tailscale up flags, e.g. --advertise-tags=tag:self-host
# --- Self-hosted GitHub Actions runner (#1205; requires --profile runners) ---
# NOT part of the default recommended stack -- it is an OPTIONAL profile. Pick a deployment mode:
# 1. App-only VPS (recommended default): gittensory + Postgres/Redis/Qdrant/observability, no runners here.
# Use GitHub-hosted CI for your own repos, or a separate runner host (below).
# 2. Separate runner host: run `--profile runners` on a DIFFERENT machine from the review stack. CI load
# never contends with the app for CPU/disk/network.
# 3. GitHub-hosted CI: simplest and safest for small teams -- no runner host to manage at all.
# CI runners sharing a VPS with the review stack CAN starve reviews: a burst of CI jobs (vitest coverage,
# wrangler builds, migration checks...) will happily consume every spare CPU cycle the app needs to process
# webhooks and AI review calls, discovered running exactly this pattern in production -- 3 uncapped runner
# containers on an 8-vCPU box left the app starved under load. If you DO need runners on this same host
# (mode 2 above, but co-located anyway, or you're testing this locally), see docker-compose.override.yml.example
# for a proven CPU-priority pattern (cpu_shares + a per-container cpus ceiling) before scaling replicas up.
# Blank tokens/URLs are valid until --profile runners is enabled.
# RUNNER_TOKEN= # runner registration token (Settings → Actions → Runners → New)
# RUNNER_REPO_URL=https://github.com/org/repo
# RUNNER_ACCESS_TOKEN= # PAT with repo scope (alternative to RUNNER_TOKEN)
# RUNNER_SCOPE=repo # repo | org | enterprise
# RUNNER_NAME=loopover-runner
# RUNNER_LABELS=self-hosted,linux
# RUNNER_MEM_LIMIT=2g # per-runner-container memory ceiling; raise for memory-heavy CI jobs
# --- Docker disk hygiene (#audit-rate-headroom / #selfhost-runtime-pressure) ---
# Build cache and unused images accumulate fast on a box that builds from source or runs CI runners; a root
# disk over ~80-85% full slows down the WHOLE host (fsync latency, container scheduling), not just Docker.
# Install the systemd timer (systemd/loopover-docker-prune.{service,timer}.example) to reclaim stopped
# containers, unused images, and build cache on a schedule -- never volumes/application data. Run it
# manually at any time with `sh scripts/selfhost-docker-prune.sh`, or preview first with `--dry-run`.
# LOOPOVER_DOCKER_PRUNE_RETAIN_HOURS=168 # age floor before something unused is eligible for pruning (7 days)
# --- Observability: metrics + alerts + logs (#1206; requires --profile observability) ---
# The observability profile starts Prometheus (scrapes /metrics) + Alertmanager (alert rules in
# prometheus/rules/, routing in alertmanager/alertmanager.yml — silent until you fill in a receiver) +
# Loki + Promtail (ship every container's logs to Loki) + Grafana (dashboards for metrics AND logs).
# GRAFANA_ADMIN_PASSWORD= # REQUIRED at runtime when using --profile observability; generate a strong value
# PROMETHEUS_RETENTION_TIME=90d # metrics history window (a full quarter for trend/capacity review).
# # Deliberately longer than Loki's 14d / Tempo's 7d: the TSDB
# # compresses samples to ~1-2 bytes each, so metrics cost far less
# # disk per retained day than raw logs or span trees (#1828).
# GRAFANA_LOCAL_SMOKE_PASSWORD= # optional local-only fallback for smoke tests; never expose Grafana with this
#
# Maintainer dashboards (in addition to the infra dashboard):
# • "Reviews & PRs (maintainer)" — per-repo + combined PR/review analytics from a redacted reporting DB export.
# • "Claude usage (OTEL)" — cost/tokens/model/effort from the review CLI's OpenTelemetry export (see below).
# • "Resource hub" — links to every integrated service.
# GRAFANA_REPORTING_EXPORT_INTERVAL_SECONDS=30 # refresh cadence for the redacted reporting SQLite export
# LOOPOVER_REPORTING_SOURCE_DB=/appdb/loopover.sqlite # if DATABASE_PATH=/data/custom.sqlite, set /appdb/custom.sqlite
# LOOPOVER_REPORTING_DB=/reporting/loopover-reporting.sqlite # where the redacted export is written (Grafana reads this)
#
# AMS (loopover-miner) ledger dashboards — only useful when a miner ALSO runs on this same host (see
# packages/loopover-miner/docs/observability.md). Requires --profile ams-observability, separate from the
# general observability profile above: an engine-only deployment has nothing for this exporter to read.
# Grafana never mounts the live ledgers directly — this exporter reads them read-only and writes a redacted
# snapshot (free-form attempt_log_events.reason/.payload_json dropped) into the same reporting volume.
# LOOPOVER_MINER_CONFIG_DIR=~/.config/loopover-miner # host dir the exporter mounts read-only at /ams-ledgers
# LOOPOVER_AMS_REPORTING_EXPORT_INTERVAL_SECONDS=30 # refresh cadence for the redacted AMS ledger exports
# Path overrides for the same exporter — in-container paths, not host paths. The source pair lives under the
# read-only /ams-ledgers mount above; the reporting pair is written into the shared reporting volume. Override
# only if your miner names its ledgers differently.
# LOOPOVER_AMS_ATTEMPT_LOG_SOURCE_DB=/ams-ledgers/attempt-log.sqlite3
# LOOPOVER_AMS_PREDICTION_LEDGER_SOURCE_DB=/ams-ledgers/prediction-ledger.sqlite3
# LOOPOVER_AMS_ATTEMPT_LOG_REPORTING_DB=/reporting/ams-attempt-log.sqlite
# LOOPOVER_AMS_PREDICTION_LEDGER_REPORTING_DB=/reporting/ams-prediction-ledger.sqlite
#
# Claude usage telemetry → OTEL collector → Prometheus → the Claude usage dashboard. OFF by default.
# CLAUDE_CODE_ENABLE_TELEMETRY=1 # enable; needs --profile observability (starts the otel-collector)
# OTEL_TRACES_EXPORTER=otlp # also enables app job/provider spans → Tempo; empty = no app traces
# OTEL_TRACES_SAMPLER=parentbased_traceidratio
# OTEL_TRACES_SAMPLER_ARG=0.05 # sample 5% of root traces when using parentbased_traceidratio
# --- Sentry error tracking (optional) ---
# SENTRY_DSN= # enables self-host Sentry capture; unset = complete no-op
# SENTRY_DSN_FILE= # optional mounted secret file; the generic *_FILE loader
# # (src/selfhost/load-file-secrets.ts) resolves it into SENTRY_DSN
# SENTRY_ENVIRONMENT=production # the code's own default (src/selfhost/sentry.ts); set any label you
# # want to slice by in Sentry (staging, selfhost, ...) — nothing
# # behaves differently per value, it only tags the events
# SENTRY_SERVER_NAME= # clean name for THIS instance in Sentry (e.g. loopover-us-east);
# # unset defaults to the OS hostname — never the public-origin URL
# SENTRY_TRACES_SAMPLE_RATE= # traces/spans are off when blank/unset; errors still report. Set a LOW
# # rate (e.g. 0.05) with SENTRY_DSN to sample review tracing: each
# # sampled review emits a connected trace — the queue-job span
# # (whole-review latency) with the AI-provider span nested — so you can
# # filter slow or failed STAGES in Sentry without reading scattered logs.
# # Spans carry only safe dimensions (repo, job type, provider/model);
# # never prompts, diffs, tokens, or bodies.
# SENTRY_RELEASE= # custom images only: set this ONLY when you uploaded source maps for
# # the exact built bundle under this exact release id. Future official
# # images bake LOOPOVER_VERSION=gittensory-selfhost@<version>, so do
# # not override SENTRY_RELEASE for those images.
# OTEL_METRIC_EXPORT_INTERVAL=10000 # ms between metric exports (default 10s here; CLI default is 60s)
# OTEL_METRICS_EXPORTER=otlp # metrics-side counterpart to OTEL_TRACES_EXPORTER; empty = no app metrics
# OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 # override only for an external collector
# OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf # wire protocol for the endpoint above (http/protobuf | grpc)
# OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=cumulative # cumulative is what Prometheus expects; change
# # only for a backend that requires delta temporality
# OTEL_SERVICE_NAME=gittensory-selfhost
#
# Live upstream PR/issue census (the GitHub data source). Install is automatic (GF_INSTALL_PLUGINS); add the
# data source after Grafana is up with: ./scripts/setup-github-datasource.sh (reads GITHUB_TOKEN below).
# GITHUB_TOKEN=<read-only-github-token> # fine-grained PAT: Pull requests:read, Issues:read, Contents:read
#
# In-Grafana Sentry error/issue visualization (#5369, distinct from SENTRY_DSN above, which is for error
# REPORTING). Install is automatic (GF_INSTALL_PLUGINS); add the data source after Grafana is up with:
# ./scripts/setup-sentry-datasource.sh (reads the two vars below). SENTRY_DSN is NOT reusable here -- a DSN
# authenticates event ingestion, not the read/query API this datasource needs.
# SENTRY_API_TOKEN=<sentry-internal-integration-token> # Sentry -> Settings -> Developer Settings -> Custom
# # Integrations -> New Internal Integration, with Read access on
# # Project, Issue & Event, and Organization scopes
# SENTRY_ORG_SLUG=<your-sentry-org-slug>
# SENTRY_API_URL=https://sentry.io # override only for a self-hosted Sentry instance
#
# Discord notifications. Alertmanager → Discord (system/stack alerts) is configured in alertmanager/alertmanager.yml.
# The ENGINE posts a per-repo review summary when it publishes a review — set a per-repo map and/or a global fallback:
# DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/... # global fallback for any repo without its own
# DISCORD_REPO_WEBHOOKS='{"owner/repoA":"https://discord.com/api/webhooks/...","owner/repoB":"https://..."}' # per-repo,
# single-quoted: a pretty-printed/spaced JSON value here breaks any script that sources .env (e.g. setup-github-datasource.sh) --
# bash treats an unquoted space as a word boundary mid-assignment, so a compact one-liner isn't enough on its own to stay safe.
#
# Slack notifications: the same per-action events (merged/closed/manual) as Discord above, posted as a Block
# Kit section to one Slack channel. Unset = no Slack notifications. Unlike Discord there is no per-repo map
# today — every repo shares this one webhook.
# SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
#
# Sentry error tracking is documented once, under "--- Sentry error tracking (optional) ---" above (#6289).
# --- AI review backend (optional; without AI_PROVIDER reviews run deterministically) ---
# AI_SUMMARIES_ENABLED=true
# The self-host image bundles the Claude Code and Codex CLIs by default. Credentials and provider choice remain
# runtime-only: set AI_PROVIDER plus the provider-specific auth below. Set INSTALL_AI_CLIS=false only for a
# custom minimal local build that will never use the subscription CLI providers.
# INSTALL_AI_CLIS=true
# INSTALL_VISUAL_REVIEW=false # sibling build arg: installs puppeteer-core for a LOCAL build.
# # Official release images already bundle it, so this only matters
# # when you build the image yourself and want LOOPOVER_REVIEW_SCREENSHOTS
# # (the --profile visual-review browserless service is the alternative).
#
# Deprecated shared AI_* knobs are intentionally rejected at startup:
# AI_BASE_URL, AI_API_KEY, AI_MODEL, AI_EFFORT, AI_TIMEOUT_MS. Use the explicit
# provider-specific variables below so Claude, Codex, Ollama, OpenAI, and
# Anthropic settings cannot be mixed up.
#
# AI_PROVIDER=ollama # ollama | openai-compatible | openai | anthropic | claude-code |
# # codex. A comma-list is a FALLBACK chain by default:
# # AI_PROVIDER=codex,claude-code runs Codex first and only spends
# # Claude Code tokens if Codex fails/exhausts.
# AI_DUAL_REVIEW=0 # opt-in only: set to 1/true/yes/on when the first two providers
# # should run as independent reviewers instead of fallback.
# AI_COMBINE=synthesis # dual-review mode only: single | consensus | synthesis.
# # consensus = block only when BOTH flag a defect (lone flag → hold).
# # synthesis = both review, then ONE merged decision.
# AI_ON_MERGE=either # synthesis merge rule: either (block if EITHER reviewer flags) |
# # both (block only when both do). Ignored unless AI_COMBINE=synthesis.
# AI_DAILY_NEURON_BUDGET=10000000 # daily spend cap (unit name is a Cloudflare Workers AI "neurons"
# # holdover; applied as a provider-agnostic heuristic budget) shared
# # by AI summaries, the free/default consensus-defect reviewer pair,
# # and the AI-slop scorer. Default 10,000,000/day; clamped to
# # [0, 10000000]. Only meters the free/default-reviewer path —
# # BYOK/provider calls above bill the maintainer's own account and
# # are not counted against this.
# Ollama reviewer (AI_PROVIDER=ollama). Defaults: OLLAMA_AI_BASE_URL=http://localhost:11434/v1,
# OLLAMA_AI_MODEL=llama3.1, no API key. Set the base URL to http://ollama:11434/v1 when using the compose
# --profile ollama service.
# OLLAMA_AI_BASE_URL=http://ollama:11434/v1
# OLLAMA_AI_API_KEY=
# OLLAMA_AI_MODEL=llama3.1
#
# Ollama concurrency/residency (--profile ollama, docker-compose.yml; #4327/#4335). Defaults sized for a
# single GPU running an embed model + one vision model concurrently -- see the comment above the ollama
# service in docker-compose.yml for the full reasoning.
# OLLAMA_NUM_PARALLEL=2 # concurrent requests per loaded model
# OLLAMA_MAX_LOADED_MODELS=2 # distinct models kept resident at once
# OLLAMA_KEEP_ALIVE=30m # how long an idle model stays loaded before eviction
# OLLAMA_NUM_BATCH=8192 # bge-m3's real context window; Ollama's own default (2048) silently
# # drops any RAG chunk larger than that ("input (N tokens) is too
# # large to process") instead of erroring loudly
# OLLAMA_FLASH_ATTENTION=1 # usually auto-enabled already on supported GPUs (check the ollama
# # container's boot log for "Flash Attention was auto, set to
# # enabled" to confirm) -- set explicitly anyway, because it is
# # REQUIRED alongside OLLAMA_KV_CACHE_TYPE below for KV-cache
# # quantization to actually take effect; setting KV_CACHE_TYPE
# # alone without this does nothing.
# OLLAMA_KV_CACHE_TYPE=q8_0 # f16 (default) | q8_0 | q4_0. q8_0 roughly HALVES per-context KV-
# # cache VRAM (confirmed on an RTX A5000: 4608 MiB at f16 -> 2448
# # MiB at q8_0 for one loaded context) with published perplexity
# # impact of 0.002-0.05 -- undetectable in practice. Frees real
# # headroom for more concurrent models or larger contexts on a
# # VRAM-constrained GPU. q4_0 quarters it further at more
# # noticeable quality cost; q8_0 is the recommended default for
# # self-host GPU deployments.
#
# Generic OpenAI-compatible reviewer (AI_PROVIDER=openai-compatible). Defaults:
# OPENAI_COMPATIBLE_AI_BASE_URL=http://localhost:11434/v1, OPENAI_COMPATIBLE_AI_MODEL=llama3.1.
# OPENAI_COMPATIBLE_AI_BASE_URL=http://localhost:11434/v1
# OPENAI_COMPATIBLE_AI_API_KEY=
# OPENAI_COMPATIBLE_AI_MODEL=llama3.1
#
# OpenAI API reviewer (AI_PROVIDER=openai). Defaults: OPENAI_AI_BASE_URL=https://api.openai.com/v1,
# OPENAI_AI_MODEL=gpt-5.5 unless set here.
# OPENAI_API_KEY=
# OPENAI_AI_BASE_URL=https://api.openai.com/v1
# OPENAI_AI_MODEL=gpt-5.5
#
# Anthropic API reviewer (AI_PROVIDER=anthropic). Defaults: ANTHROPIC_AI_BASE_URL=https://api.anthropic.com,
# ANTHROPIC_AI_MODEL=claude-sonnet-4-6 unless set here.
# ANTHROPIC_API_KEY=
# ANTHROPIC_AI_BASE_URL=https://api.anthropic.com
# ANTHROPIC_AI_MODEL=claude-sonnet-4-6
#
# Claude Code subscription reviewer (AI_PROVIDER=claude-code).
# CLAUDE_CODE_OAUTH_TOKEN= # from `claude setup-token`
# CLAUDE_AI_MODEL=claude-sonnet-4-6 # any `claude` CLI model id/alias, e.g. sonnet | opus | claude-opus-4-8
# CLAUDE_AI_EFFORT=medium # low | medium | high | xhigh | max
# CLAUDE_AI_TIMEOUT_MS= # override CLI timeout in ms; unset scales by effort (low/medium 120s, high 240s, xhigh 360s, max 600s)
#
# Codex (ChatGPT subscription) reviewer is fail-closed by default for self-host PR review: `codex exec` stores its
# OAuth credential in auth.json on the same filesystem that prompt-influenced reviews can read. Isolated maintainer
# deployments can opt in explicitly after mounting auth at /data/codex (the image exposes it as ~/.codex).
# LOOPOVER_ENABLE_UNSAFE_CODEX_REVIEWER=1 # strict "1"-only opt-in; any other value stays fail-closed
# GITTENSORY_ENABLE_UNSAFE_CODEX_REVIEWER=1 # no longer read (removed by #4777)
# Do NOT set CODEX_HOME for the app container; the provider rejects it so credentials are not advertised in env.
# CODEX_AI_MODEL=gpt-5.5 # omit to let the Codex account default choose; set explicitly for repeatable reviews
# CODEX_AI_EFFORT=medium # low | medium | high | xhigh. `max` is accepted and maps to xhigh.
# CODEX_AI_TIMEOUT_MS= # override CLI timeout in ms; unset scales by effort (low/medium 120s, high 240s, xhigh 360s)
# # Codex service speed is standard by default. No fast/priority tier is requested by this stack.
# AI_EMBED_MODEL=bge-m3:latest # embedding model for RAG (openai-compatible /embeddings). Its output
# # dimension must match QDRANT_DIM (1024 for bge-m3). Used only when
# # RAG is enabled (LOOPOVER_REVIEW_RAG + allowlist).
# AI_EMBED_BASE_URL= # route embeddings to a SEPARATE openai-compatible endpoint instead
# # of the review chain's own AI provider — e.g. a dedicated local
# # Ollama for embeddings while Claude/Codex handle review. Unset =
# # embeddings use the same provider as everything else; setting
# # AI_EMBED_MODEL above alone does nothing without this.
# AI_EMBED_API_KEY= # bearer credential for AI_EMBED_BASE_URL, if it requires one.
# AI_VISION_MODEL=qwen3-vl:8b-instruct # vision-language model for the visual-vision advisory (#4111/#4335)
# # analyzing before/after PR screenshots. Only used when
# # AI_VISION_BASE_URL is also set.
# AI_VISION_BASE_URL= # route visual-vision to a SEPARATE openai-compatible endpoint that
# # can actually see images — e.g. a local Ollama running a VLM. The
# # subscription CLIs (claude-code/codex) cannot consume inline image
# # bytes, so before this was set, visual-vision required a maintainer
# # BYOK (anthropic/openai) key; this gives self-host a local option.
# # Unset = visual-vision falls back to BYOK-only.
# AI_VISION_API_KEY= # bearer credential for AI_VISION_BASE_URL, if it requires one.
# AI_ADVISORY_MODEL=qwen3:8b # local-inference model for advisory-only capabilities (#4364):
# # slop advisory, e2e test-gen, issue planner, AI summaries -- none
# # are gate-blocking, so none need frontier-model accuracy. Only
# # used when AI_ADVISORY_BASE_URL is also set.
# AI_ADVISORY_BASE_URL= # route the four advisory-only capabilities above to a SEPARATE
# # openai-compatible endpoint (e.g. a local Ollama) instead of the
# # shared frontier claude-code/codex chain. Unset = every advisory
# # capability stays on the shared chain. Even when set, a
# # capability only actually routes through it once the repo's
# # .loopover.yml settings.advisoryAiRouting also opts it in
# # (global default + per-repo override) -- this binding alone
# # changes nothing.
# AI_ADVISORY_API_KEY= # bearer credential for AI_ADVISORY_BASE_URL, if it requires one.
# --- Gittensory Orb (#1255; ALWAYS-ON fleet-calibration telemetry) ---
# TELEMETRY NOTICE: running this self-hosted image contributes anonymized gate-calibration data to
# gittensory's central collector. This is ON BY DEFAULT and has no opt-out flag — it is part of the
# self-hosting contract: install the GitHub App, and your instance reports fleet-calibration signal so the
# gate can be tuned from real outcomes across all self-hosters. It activates automatically once your App is
# configured (no App = nothing is sent). There is NO separate Orb App and NO setup wizard.
#
# WHAT IS SENT (per resolved PR, hourly): the gate verdict, the realized outcome (merged/closed), a reversal
# flag, a bucketed reason category, and cycle time. NEVER sent: repo/owner/PR names, commit SHAs, code,
# diffs, comments, or logins. Repo/PR identifiers are HMAC-anonymized with a DEDICATED, randomly-generated
# per-instance secret created once and stored in your instance's own database (never your App private key or
# webhook secret) — and the collector never holds it, so even gittensory (running the collector) can never
# de-anonymize them.
# The export carries no shared key; the collector treats it as untrusted, rate-limited, aggregate-only data.
# ORB_AIR_GAP=false # air-gapped/OFFLINE deployments only: compute locally, never send
# ORB_ANONYMIZE=true # HMAC-hash repo/PR before export (default true; false = raw names)
# ORB_COLLECTOR_URL=https://api.loopover.ai/v1/orb/ingest # loopover's hosted collector (default; override for your own)
# ORB_COLLECTOR_TOKEN= # bearer credential for a private/self-hosted collector (ORB_COLLECTOR_URL
# # above). Unset when using loopover's own hosted collector.
#
# Token broker (optional): get GitHub tokens from the central Orb (you installed the Orb App) instead of running
# your own GitHub App. Set the enrollment secret the operator issued for your install; unset = use your own App key.
# ORB_ENROLLMENT_SECRET= # one-time enrollment secret (a secret — keep it out of version control)
# ORB_BROKER_URL=https://api.loopover.ai # the Orb broker base (default; override for a private deployment)
# ORB_RELAY_MODE=push # push | pull. push registers a public relay URL the broker calls;
# # pull is the right fit behind NAT/tailnet — your instance drains
# # events outbound instead of exposing an inbound endpoint. Default: push.