diff --git a/apps/loopover-ui/content/docs/ai-summaries.mdx b/apps/loopover-ui/content/docs/ai-summaries.mdx
index 4098925661..71bd6e8901 100644
--- a/apps/loopover-ui/content/docs/ai-summaries.mdx
+++ b/apps/loopover-ui/content/docs/ai-summaries.mdx
@@ -1,6 +1,7 @@
---
title: Optional AI summaries
description: A short natural-language summary over the deterministic response. Off by default. Never the source of truth.
+eyebrow: Roadmap · exploring
---
## The rule
diff --git a/apps/loopover-ui/content/docs/ams-config-precedence.mdx b/apps/loopover-ui/content/docs/ams-config-precedence.mdx
index 5b3d749fba..3dca2e4ce1 100644
--- a/apps/loopover-ui/content/docs/ams-config-precedence.mdx
+++ b/apps/loopover-ui/content/docs/ams-config-precedence.mdx
@@ -1,6 +1,7 @@
---
title: Miner config precedence
description: How AMS layers configuration across per-repo goal spec, operator env, CLI flags, and operator policy files -- the order each concern actually implements today.
+eyebrow: Maintainers
---
AMS does **not** have a single `config.js` resolver. Configuration is layered by concern across
@@ -87,8 +88,8 @@ First existing file wins (engine `MINER_GOAL_SPEC_FILENAMES`):
This is an **AND** requirement, not "last writer wins". Either side missing or malformed →
- dry-run. There is also **no CLI flag wired to governor live mode** today —
- `attempt --live` / `loop --live` affect coding-agent spawn mode only (below).
+ dry-run. There is also **no CLI flag wired to governor live mode** today — `attempt --live` /
+ `loop --live` affect coding-agent spawn mode only (below).
### Coding-agent execution mode (spawn the driver?)
@@ -151,9 +152,9 @@ cached, so a later call retries rather than staying stuck).
requiring auth fail the same way they did before this feature existed).
- This is a distinct concern from "Discover forge credential env var name" above, which resolves
- the *name* of an env var to read, not the token *value* itself; `discover --token-env` is
- unaffected by this section.
+ This is a distinct concern from "Discover forge credential env var name" above, which resolves the
+ *name* of an env var to read, not the token *value* itself; `discover --token-env` is unaffected
+ by this section.
### Local SQLite store paths
diff --git a/apps/loopover-ui/content/docs/ams-deployment.mdx b/apps/loopover-ui/content/docs/ams-deployment.mdx
index 7347bb922c..0e4af4c64e 100644
--- a/apps/loopover-ui/content/docs/ams-deployment.mdx
+++ b/apps/loopover-ui/content/docs/ams-deployment.mdx
@@ -1,6 +1,7 @@
---
title: AMS deployment guide
description: Deploy @loopover/miner in laptop mode (single machine, zero Docker) or fleet mode (containerized workers) -- both 100% client-side, credentials never baked into images.
+eyebrow: Maintainers
---
Two form factors for running `@loopover/miner`: **laptop mode** (single machine, zero Docker) and
@@ -57,9 +58,9 @@ loopover-miner init --interactive # optional: guided prompt for GITHUB_TOKEN
`init --interactive` offers "Authorize with GitHub" (device flow — visit a URL, enter a short
code, no token to copy or paste) as its first option once the miner's GitHub App OAuth client is
configured; the original pasted-PAT prompt stays available as option 2, and is what the wizard
- falls back to automatically on any device-flow failure. Unconfigured, the wizard is
- byte-identical to the pasted-token-only prompt. Either way, the resulting `GITHUB_TOKEN` acts as
- your own GitHub account — there is no separate bot identity.
+ falls back to automatically on any device-flow failure. Unconfigured, the wizard is byte-identical
+ to the pasted-token-only prompt. Either way, the resulting `GITHUB_TOKEN` acts as your own GitHub
+ account — there is no separate bot identity.
### Expected layout after first use
@@ -150,9 +151,9 @@ The image entrypoint is `loopover-miner`; pass subcommands after the image name
config` and any full-env dump of the running container. For Docker Swarm/Kubernetes-managed
secrets (mounted as a file, e.g. at `/run/secrets/github_token`), set `GITHUB_TOKEN_FILE` to that
mount path instead — the miner reads and trims the file's contents at startup and uses it exactly
- as if `GITHUB_TOKEN` had been set directly. If both are set, the plain `GITHUB_TOKEN` value
- always wins. A missing or unreadable `GITHUB_TOKEN_FILE` fails the container fast with a clear
- error naming the file path, rather than silently proceeding with no credential.
+ as if `GITHUB_TOKEN` had been set directly. If both are set, the plain `GITHUB_TOKEN` value always
+ wins. A missing or unreadable `GITHUB_TOKEN_FILE` fails the container fast with a clear error
+ naming the file path, rather than silently proceeding with no credential.
`docker compose -f docker-compose.miner.yml up -d --scale miner=N` gives every replica the
**same** `miner-data` volume — and the miner's SQLite ledgers are **not** safe for concurrent
- access, so N replicas on one volume will contend/corrupt. To run N **isolated** workers, give
- each its own state: run N separate compose projects (`docker compose -p miner-1 …`, `-p miner-2
- …` — `-p` namespaces the volume) or point each at a distinct `LOOPOVER_MINER_CONFIG_DIR` on its
- own mount. For built-in isolated horizontal scaling, use the Kubernetes StatefulSet in
+ access, so N replicas on one volume will contend/corrupt. To run N **isolated** workers, give each
+ its own state: run N separate compose projects (`docker compose -p miner-1 …`, `-p miner-2 …` —
+ `-p` namespaces the volume) or point each at a distinct `LOOPOVER_MINER_CONFIG_DIR` on its own
+ mount. For built-in isolated horizontal scaling, use the Kubernetes StatefulSet in
[`k8s/`](https://github.com/JSONbored/loopover/tree/main/k8s) (per-pod volumes).
diff --git a/apps/loopover-ui/content/docs/ams-discovery-plane.mdx b/apps/loopover-ui/content/docs/ams-discovery-plane.mdx
index 999feb21cf..eb494a27bb 100644
--- a/apps/loopover-ui/content/docs/ams-discovery-plane.mdx
+++ b/apps/loopover-ui/content/docs/ams-discovery-plane.mdx
@@ -1,6 +1,7 @@
---
title: Hosted discovery plane — operator guide (opt-in)
description: How a loopover-miner instance opts into the optional hosted discovery-index plane, what it may send, and what never leaves the operator's machine.
+eyebrow: Maintainers
---
Operator-facing guide for the **optional** hosted discovery-index plane. This is the client/miner
diff --git a/apps/loopover-ui/content/docs/ams-env-reference.mdx b/apps/loopover-ui/content/docs/ams-env-reference.mdx
index 90addd381d..86f261d6a8 100644
--- a/apps/loopover-ui/content/docs/ams-env-reference.mdx
+++ b/apps/loopover-ui/content/docs/ams-env-reference.mdx
@@ -1,6 +1,7 @@
---
title: AMS environment variable reference
description: Every LOOPOVER_MINER_* / MINER_* env var the miner reads, where it's first referenced in code, and its default -- generated from source, never hand-copied.
+eyebrow: Maintainers
---
import { AMS_ENV_REFERENCE_MARKDOWN } from "@/lib/ams-env-reference";
diff --git a/apps/loopover-ui/content/docs/ams-fleet-manifest.mdx b/apps/loopover-ui/content/docs/ams-fleet-manifest.mdx
index 26993590ac..bb83848515 100644
--- a/apps/loopover-ui/content/docs/ams-fleet-manifest.mdx
+++ b/apps/loopover-ui/content/docs/ams-fleet-manifest.mdx
@@ -1,9 +1,10 @@
---
title: Fleet run-manifest
description: The top-level config a fleet operator authors to run the miner across many repos at once -- which repos are in scope and how the worktree/concurrency budget is split.
+eyebrow: Maintainers
---
-The **fleet run-manifest** is the top-level config a *fleet operator* authors to run the miner
+The **fleet run-manifest** is the top-level config a _fleet operator_ authors to run the miner
across many repos at once: it declares which repos are in scope for a fleet run and how a finite
worktree/concurrency budget is split between them. It is parsed by
`parseFleetRunManifestContent` / `parseFleetRunManifest` in `@loopover/engine`
@@ -50,7 +51,7 @@ opposite direction of intent:
This module produces only the parsed, typed manifest. Driving the fleet concurrency allocator
from it is the allocator's own concern, and the cross-repo `portfolio-queue.js` backlog reads the
-same repo list — both *consume* this manifest; neither wiring lives here.
+same repo list — both _consume_ this manifest; neither wiring lives here.
## Example (`fleet-run.yml`)
diff --git a/apps/loopover-ui/content/docs/ams-goal-spec.mdx b/apps/loopover-ui/content/docs/ams-goal-spec.mdx
index 505c3705ec..ad5b726c24 100644
--- a/apps/loopover-ui/content/docs/ams-goal-spec.mdx
+++ b/apps/loopover-ui/content/docs/ams-goal-spec.mdx
@@ -1,6 +1,7 @@
---
title: "MinerGoalSpec (.loopover-miner.yml)"
description: Per-repo configuration telling an autonomous LoopOver miner what to look for and how to behave when targeting a repo -- the full field reference.
+eyebrow: Maintainers
---
Per-repo configuration telling an autonomous LoopOver miner what to look for and how to behave
@@ -17,8 +18,8 @@ Discovery order (first match wins):
- `.github/loopover-miner.json`
- Every field is optional. Unknown keys are ignored; a malformed field falls back to its
- documented default with a warning — a broken file never hard-fails the miner.
+ Every field is optional. Unknown keys are ignored; a malformed field falls back to its documented
+ default with a warning — a broken file never hard-fails the miner.
## Relationship to `.loopover.yml`
@@ -113,13 +114,12 @@ Per-repo kill-switch consulted by the Governor chokepoint before every write act
Per-repo dry-run/live execution opt-in consulted by the Governor chokepoint.
- A freshly-configured miner always defaults to dry-run (observe/log only, never execute a
- write) — this field is the only per-repo path to live mode, and it alone is not sufficient: the
- miner's own operator must also separately opt in globally (env var
- `LOOPOVER_MINER_LIVE_MODE=live`) before writes actually execute. A repo that wants to guarantee
- it never receives live automated writes, regardless of any operator's global setting, should use
- `killSwitch.paused: true` instead — the kill-switch always takes precedence over any live-mode
- opt-in.
+ A freshly-configured miner always defaults to dry-run (observe/log only, never execute a write) —
+ this field is the only per-repo path to live mode, and it alone is not sufficient: the miner's own
+ operator must also separately opt in globally (env var `LOOPOVER_MINER_LIVE_MODE=live`) before
+ writes actually execute. A repo that wants to guarantee it never receives live automated writes,
+ regardless of any operator's global setting, should use `killSwitch.paused: true` instead — the
+ kill-switch always takes precedence over any live-mode opt-in.
- `liveModeOptIn` (string or `null`, default: `null`) — must equal EXACTLY the literal `"live"` to
diff --git a/apps/loopover-ui/content/docs/ams-kill-switch-incident.mdx b/apps/loopover-ui/content/docs/ams-kill-switch-incident.mdx
index 2326544be7..a7d4bac851 100644
--- a/apps/loopover-ui/content/docs/ams-kill-switch-incident.mdx
+++ b/apps/loopover-ui/content/docs/ams-kill-switch-incident.mdx
@@ -1,6 +1,7 @@
---
title: Kill-switch incident runbook
description: Detect, activate, and audit a misbehaving Rent-a-Loop miner after the kill-switch stops it — grounded in the shipped #5670 mechanism.
+eyebrow: Maintainers
---
Operator runbook for a **misbehaving autonomous miner loop** once the emergency kill-switch has
@@ -17,9 +18,9 @@ been (or must be) engaged. Grounded in the shipped mechanism from #5670 (`packag
Two independent triggers compose into one scope per repo:
-| Trigger | How | Scope reported |
-| --- | --- | --- |
-| Global | `LOOPOVER_MINER_KILL_SWITCH` env var (`1` / `true` / `yes` / `on`) | `"global"` — every repo |
+| Trigger | How | Scope reported |
+| -------- | -------------------------------------------------------------------------------- | ------------------------- |
+| Global | `LOOPOVER_MINER_KILL_SWITCH` env var (`1` / `true` / `yes` / `on`) | `"global"` — every repo |
| Per-repo | `killSwitch.paused: true` in that repo's `.loopover-miner.yml` (`MinerGoalSpec`) | `"repo"` — that repo only |
A global halt **always wins** and is reported as `"global"` even when the per-repo flag is also set.
diff --git a/apps/loopover-ui/content/docs/ams-observability.mdx b/apps/loopover-ui/content/docs/ams-observability.mdx
index 555ba2d961..4446d987cf 100644
--- a/apps/loopover-ui/content/docs/ams-observability.mdx
+++ b/apps/loopover-ui/content/docs/ams-observability.mdx
@@ -1,6 +1,7 @@
---
title: Observing your miner
description: Point Grafana at redacted AMS reporting exports to see attempt and prediction history without exposing the miner's live local ledgers.
+eyebrow: Maintainers
---
How to point Grafana at redacted miner reporting exports to see attempt and prediction history
@@ -73,9 +74,9 @@ visualize AMS activity, add a dashboard JSON there — or import one at runtime
Ledger` datasources above.
- Panels should query only the redacted reporting schema (e.g. `SELECT * FROM
- attempt_log_events`), never a `payload_json`/`reason` column — the exporter drops both, so a
- panel referencing them returns no such column.
+ Panels should query only the redacted reporting schema (e.g. `SELECT * FROM attempt_log_events`),
+ never a `payload_json`/`reason` column — the exporter drops both, so a panel referencing them
+ returns no such column.
## Prometheus metrics (opt-in)
diff --git a/apps/loopover-ui/content/docs/ams-operations-runbook.mdx b/apps/loopover-ui/content/docs/ams-operations-runbook.mdx
index 54339ec160..5d66ac5674 100644
--- a/apps/loopover-ui/content/docs/ams-operations-runbook.mdx
+++ b/apps/loopover-ui/content/docs/ams-operations-runbook.mdx
@@ -1,6 +1,7 @@
---
title: AMS operations runbook
description: Recover from SQLite lock contention, ledger corruption, and post-upgrade schema migrations in loopover-miner's local state.
+eyebrow: Maintainers
---
Operator-facing runbook for **local SQLite state**: what the concurrency guarantees actually mean,
@@ -45,7 +46,7 @@ Default **5000 ms**; overridable per-test only — production stores always use
{
title: "Two short-lived writers, same file",
description:
- "E.g. a CLI command finishing while loop is idle, or Grafana reading while the miner appends. SQLite waits up to 5 seconds for the lock, then proceeds or surfaces \"database is locked\".",
+ 'E.g. a CLI command finishing while loop is idle, or Grafana reading while the miner appends. SQLite waits up to 5 seconds for the lock, then proceeds or surfaces "database is locked".',
},
{
title: "Append-only ledgers",
@@ -67,9 +68,8 @@ Default **5000 ms**; overridable per-test only — production stores always use
**Invariant:** one active loop (or one intentional writer set) per state directory. Horizontal
- scale = **isolated state dirs** — separate compose projects, separate
- `LOOPOVER_MINER_CONFIG_DIR`, or the Kubernetes StatefulSet pattern in the [AMS deployment
- guide](/docs/ams-deployment).
+ scale = **isolated state dirs** — separate compose projects, separate `LOOPOVER_MINER_CONFIG_DIR`,
+ or the Kubernetes StatefulSet pattern in the [AMS deployment guide](/docs/ams-deployment).
Quick health check — `doctor` includes `laptop-state-sqlite` (file exists + readable) and
@@ -118,9 +118,9 @@ Re-run `loopover-miner doctor`. If locks persist with a single process, see **Le
below.
- Claims are local bookkeeping only. Two miners on different machines claiming the same GitHub
- issue is a **fleet coordination** problem (duplicate-cluster adjudication in the engine), not
- something SQLite resolves — split state dirs and use operational claim hygiene.
+ Claims are local bookkeeping only. Two miners on different machines claiming the same GitHub issue
+ is a **fleet coordination** problem (duplicate-cluster adjudication in the engine), not something
+ SQLite resolves — split state dirs and use operational claim hygiene.
## Backup and restore
@@ -158,7 +158,7 @@ loopover-miner doctor --json # verify afterward`}
/>
It also removes any leftover `-wal`/`-shm` sidecar files from the live directory after restoring
-each store — those hold in-flight writes from *before* the restore, and leaving them in place
+each store — those hold in-flight writes from _before_ the restore, and leaving them in place
would let SQLite silently replay stale pre-restore writes back on top of the freshly restored
file on next open.
@@ -184,10 +184,7 @@ Check disk space, permissions (`0600` file, `0700` parent), and backup tools cop
**Remediation.** Stop the miner, then back up the whole state directory (even damaged files help
post-mortems):
-
+
Choose a recovery tier:
@@ -267,8 +264,8 @@ tarball, pin the previous package version, and file an issue with the failing `u
store filename.
- Upgrade and restart **one worker/state dir at a time** so isolated workers never share a
- directory mid-migration.
+ Upgrade and restart **one worker/state dir at a time** so isolated workers never share a directory
+ mid-migration.
## Related docs
diff --git a/apps/loopover-ui/content/docs/ams-sizing.mdx b/apps/loopover-ui/content/docs/ams-sizing.mdx
index dd05079ce8..c6069ff018 100644
--- a/apps/loopover-ui/content/docs/ams-sizing.mdx
+++ b/apps/loopover-ui/content/docs/ams-sizing.mdx
@@ -1,6 +1,7 @@
---
title: Resource sizing
description: Real, measured CPU/RAM/disk numbers for laptop mode and fleet mode, so an operator can size a host or cluster from data instead of guessing.
+eyebrow: Maintainers
---
Real, measured CPU/RAM/disk numbers for laptop mode and fleet mode, so an operator can size a
@@ -24,13 +25,12 @@ captured.
Both modes measure `loopover-miner init` (laptop mode only) followed by `loopover-miner discover [...] --json` against real, small public repositories (`octocat/Hello-World`, `octocat/Spoon-Knife`) with **no `GITHUB_TOKEN`** — real, unauthenticated GitHub GET requests, metadata fan-out + deterministic ranking, no writes.
- Sizing a real coding-agent attempt would need an operator-supplied `GITHUB_TOKEN` and
- coding-agent CLI credentials, and creates a real branch/PR against a real target repository —
- not something to spend for a resource-sizing exercise, and not reproducible by a reviewer
- without supplying their own credentials. `discover` is measured instead because it is the
- dominant, always-run, network- and CPU-bound phase every mode exercises identically; `attempt`'s
- resource profile is dominated by the operator's chosen coding-agent CLI process, not by anything
- this package controls.
+ Sizing a real coding-agent attempt would need an operator-supplied `GITHUB_TOKEN` and coding-agent
+ CLI credentials, and creates a real branch/PR against a real target repository — not something to
+ spend for a resource-sizing exercise, and not reproducible by a reviewer without supplying their
+ own credentials. `discover` is measured instead because it is the dominant, always-run, network-
+ and CPU-bound phase every mode exercises identically; `attempt`'s resource profile is dominated by
+ the operator's chosen coding-agent CLI process, not by anything this package controls.
## Methodology
@@ -56,7 +56,7 @@ Both modes measure `loopover-miner init` (laptop mode only) followed by `loopove
node packages/loopover-miner/bin/loopover-miner.js init --json
LOOPOVER_MINER_CONFIG_DIR=/tmp/sizing-laptop /usr/bin/time -v \\
- node packages/loopover-miner/bin/loopover-miner.js discover octocat/Hello-World --dry-run --json
+node packages/loopover-miner/bin/loopover-miner.js discover octocat/Hello-World --dry-run --json
du -sh /tmp/sizing-laptop`}
/>
@@ -70,10 +70,10 @@ names/volumes):**
docker volume create miner-sizing-1
docker run -d --name miner-sizing-1 -v miner-sizing-1:/data/miner --entrypoint sh loopover-miner:sizing \\
- -c "loopover-miner discover octocat/Hello-World octocat/Spoon-Knife --json > /tmp/out.json 2>&1; sleep 20"
+-c "loopover-miner discover octocat/Hello-World octocat/Spoon-Knife --json > /tmp/out.json 2>&1; sleep 20"
for i in $(seq 1 20); do
- docker stats --no-stream --format '{{.Container}}\\t{{.CPUPerc}}\\t{{.MemUsage}}' miner-sizing-1
+docker stats --no-stream --format '{{.Container}}\\t{{.CPUPerc}}\\t{{.MemUsage}}' miner-sizing-1
done
docker run --rm -v miner-sizing-1:/data alpine du -ah /data`}
@@ -118,7 +118,7 @@ docker run --rm -v miner-sizing-1:/data alpine du -ah /data`}
touches the larger per-attempt stores (`attempt-log.sqlite3`, `worktree-allocator.sqlite3`,
etc.), which only grow once real attempts run.
- The 333 MB fleet image is dominated by the Node 24 base image and `node_modules`; `npm prune
- --omit=dev` in the Dockerfile already strips dev dependencies from the runtime stage.
+--omit=dev` in the Dockerfile already strips dev dependencies from the runtime stage.
See the [AMS deployment guide](/docs/ams-deployment) for the laptop-mode and fleet-mode setup
walkthroughs these numbers apply to, and the [AMS operations
diff --git a/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx b/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx
index a8b5b1fbb9..0c4a68dd18 100644
--- a/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx
+++ b/apps/loopover-ui/content/docs/ams-unattended-scheduling.mdx
@@ -1,6 +1,7 @@
---
title: Unattended scheduling & failure alerting
description: Run the miner's scheduled commands -- manage poll and discover -- unattended on cron or systemd, and alert reliably when a run fails.
+eyebrow: Maintainers
---
Operational guidance for running the miner's scheduled commands — `manage poll` and `discover` —
@@ -9,9 +10,9 @@ commands most likely to run on a schedule; everything they need is local and the
interactive prompts.
- Scope: scheduling + failure alerting for `manage poll` / `discover`. For local-state recovery
- see the [AMS operations runbook](/docs/ams-operations-runbook); for deployment layout see the
- [AMS deployment guide](/docs/ams-deployment).
+ Scope: scheduling + failure alerting for `manage poll` / `discover`. For local-state recovery see
+ the [AMS operations runbook](/docs/ams-operations-runbook); for deployment layout see the [AMS
+ deployment guide](/docs/ams-deployment).
## The exit-code contract (what to alert on)
@@ -48,12 +49,16 @@ MAILTO=you@example.com
LOOPOVER_MINER_NO_UPDATE_CHECK=1
# Poll a tracked PR every 10 minutes. The || branch fires on any non-zero exit: it logs the failing
+
# code to syslog AND re-raises it with exit "$status", so the failure stays visible to exit-status
+
# monitoring instead of being masked by logger's own success.
-*/10 * * * * /usr/local/bin/loopover-miner manage poll acme/widgets 42 --json || { status=$?; logger -t loopover-miner "manage poll failed (exit $status)"; exit "$status"; }
+
+_/10 _ \* \* \* /usr/local/bin/loopover-miner manage poll acme/widgets 42 --json || { status=$?; logger -t loopover-miner "manage poll failed (exit $status)"; exit "$status"; }
# Discover + enqueue candidate work hourly.
-0 * * * * /usr/local/bin/loopover-miner discover --search "label:good-first-issue" --json || { status=$?; logger -t loopover-miner "discover failed (exit $status)"; exit "$status"; }`}
+
+0 \* \* \* \* /usr/local/bin/loopover-miner discover --search "label:good-first-issue" --json || { status=$?; logger -t loopover-miner "discover failed (exit $status)"; exit "$status"; }`}
/>
Two cron facts to get right here:
@@ -66,8 +71,8 @@ Two cron facts to get right here:
`logger` succeeds (exit 0), so `cmd || logger …` makes the whole cron job exit 0 — any
- exit-status-based monitoring then sees success. Capture the code first (`status=$?`) and
- re-raise it (`exit "$status"`) as shown, so the real failing code survives.
+ exit-status-based monitoring then sees success. Capture the code first (`status=$?`) and re-raise
+ it (`exit "$status"`) as shown, so the real failing code survives.
## systemd (service + timer)
@@ -86,7 +91,9 @@ OnFailure=loopover-miner-alert@%n.service
[Service]
Type=oneshot
Environment=LOOPOVER_MINER_NO_UPDATE_CHECK=1
+
# A non-zero exit (2) marks the unit failed and triggers OnFailure=.
+
ExecStart=/usr/local/bin/loopover-miner discover --search "label:good-first-issue" --json`}
/>
diff --git a/apps/loopover-ui/content/docs/backtest-calibration.mdx b/apps/loopover-ui/content/docs/backtest-calibration.mdx
index 8d0d5e1ec9..41c7a89eae 100644
--- a/apps/loopover-ui/content/docs/backtest-calibration.mdx
+++ b/apps/loopover-ui/content/docs/backtest-calibration.mdx
@@ -1,6 +1,7 @@
---
title: Backtest & calibration
description: How LoopOver measures whether its own review rules are right — and backtests threshold and logic changes against real history before they ship.
+eyebrow: Core concepts
---
## What this is
diff --git a/apps/loopover-ui/content/docs/beta-onboarding.mdx b/apps/loopover-ui/content/docs/beta-onboarding.mdx
index 57bdd87469..6d947944b7 100644
--- a/apps/loopover-ui/content/docs/beta-onboarding.mdx
+++ b/apps/loopover-ui/content/docs/beta-onboarding.mdx
@@ -1,6 +1,7 @@
---
title: Beta onboarding by role
description: "Pick the lane that matches you. Each path ends in a concrete first win — install, configure, or read a report — without treating LoopOver as an official Gittensor product surface."
+eyebrow: Get started
---
diff --git a/apps/loopover-ui/content/docs/branch-analysis.mdx b/apps/loopover-ui/content/docs/branch-analysis.mdx
index 6d4c33d145..0580d493df 100644
--- a/apps/loopover-ui/content/docs/branch-analysis.mdx
+++ b/apps/loopover-ui/content/docs/branch-analysis.mdx
@@ -1,6 +1,7 @@
---
title: Branch analysis
description: LoopOver analyzes branches using metadata only. Your source code never leaves your machine.
+eyebrow: Core concepts
---
## Inputs
@@ -33,19 +34,19 @@ Authorization: Bearer ••••••••
Content-Type: application/json
{
- "login": "your-github-login",
- "repoFullName": "entrius/gittensor",
- "baseRef": "main",
- "headRef": "feat/scorer-cleanup",
- "changedFiles": [
- { "path": "src/scorer.ts", "additions": 42, "deletions": 8, "status": "modified" }
- ],
- "labels": ["scorer", "ready-for-review"],
- "linkedIssues": [421],
- "commitMessages": ["refactor scorer gating", "fix linked-issue projection"],
- "validation": [
- { "command": "npm run typecheck", "status": "passed" }
- ]
+"login": "your-github-login",
+"repoFullName": "entrius/gittensor",
+"baseRef": "main",
+"headRef": "feat/scorer-cleanup",
+"changedFiles": [
+{ "path": "src/scorer.ts", "additions": 42, "deletions": 8, "status": "modified" }
+],
+"labels": ["scorer", "ready-for-review"],
+"linkedIssues": [421],
+"commitMessages": ["refactor scorer gating", "fix linked-issue projection"],
+"validation": [
+{ "command": "npm run typecheck", "status": "passed" }
+]
}`}
/>
diff --git a/apps/loopover-ui/content/docs/capacity.mdx b/apps/loopover-ui/content/docs/capacity.mdx
index 704ca99128..7c3840b54e 100644
--- a/apps/loopover-ui/content/docs/capacity.mdx
+++ b/apps/loopover-ui/content/docs/capacity.mdx
@@ -1,6 +1,7 @@
---
title: Capacity and throughput
description: Real throughput and concurrency numbers for AMS's iterate-loop and the review-gate's PR-processing queue, so an operator can plan for load instead of guessing.
+eyebrow: Maintainers
---
Real throughput and concurrency numbers for AMS's iterate-loop and the review-gate's PR-processing
@@ -39,15 +40,18 @@ REST GETs):
items={[
{
title: "SWEEP_MAX_PRS = 3",
- description: "The recurring sweep's per-tick cap — sized for a sweep that re-runs every ~2 minutes, so a low steady-state budget compounds safely across ticks.",
+ description:
+ "The recurring sweep's per-tick cap — sized for a sweep that re-runs every ~2 minutes, so a low steady-state budget compounds safely across ticks.",
},
{
title: "ISSUE_WAKE_MAX_PRS = 25",
- description: "One-shot budget for an issue-linked wake event — a rarer trigger than a merge, so a larger one-time budget doesn't risk compounding across repeated events in one rate-limit window.",
+ description:
+ "One-shot budget for an issue-linked wake event — a rarer trigger than a merge, so a larger one-time budget doesn't risk compounding across repeated events in one rate-limit window.",
},
{
title: "MERGE_WAKE_MAX_PRS = 15",
- description: "One-shot budget for a merge-triggered wake — sized lower than the issue-wake budget because merges are a far more common trigger; a repeated-merge burst inside one rate-limit window must not compound the way the rarer issue-wake trigger safely can.",
+ description:
+ "One-shot budget for a merge-triggered wake — sized lower than the issue-wake budget because merges are a far more common trigger; a repeated-merge burst inside one rate-limit window must not compound the way the rarer issue-wake trigger safely can.",
},
]}
/>
@@ -61,21 +65,24 @@ attributes, read at `wrangler deploy` time (not runtime-configurable via `env.SO
items={[
{
title: "max_batch_size = 5",
- description: "The most jobs one batch can bundle at once — bounds how many heavy sweep/backfill jobs land in a single delivery.",
+ description:
+ "The most jobs one batch can bundle at once — bounds how many heavy sweep/backfill jobs land in a single delivery.",
},
{
title: "max_concurrency = 8",
- description: "Global consumer fan-out for multi-tenant volume (#4892) — aligned with the #4913 load-test concurrency=8 band. Per-installation admission (default limit 2) still caps how many background GitHub-budget jobs one tenant may run at once, so raising the global bound cannot let a single installation monopolize every worker slot.",
+ description:
+ "Global consumer fan-out for multi-tenant volume (#4892) — aligned with the #4913 load-test concurrency=8 band. Per-installation admission (default limit 2) still caps how many background GitHub-budget jobs one tenant may run at once, so raising the global bound cannot let a single installation monopolize every worker slot.",
},
]}
/>
- `max_batch_size × max_concurrency` (5 × 8 = 40) bounds Worker in-flight work. Rate-limit safety across
- tenants comes from two already-shipped layers: each GitHub App installation has its own REST budget,
- and `installation-concurrency-admission.ts` (#2970) caps per-install background GitHub fetches. Further
- re-tuning still benefits from live `audit_events` / dashboard volume data when available — these defaults
- are the architecture-justified baseline after the #4903–#4913 scaling work, not a production-volume fit.
+ `max_batch_size × max_concurrency` (5 × 8 = 40) bounds Worker in-flight work. Rate-limit safety
+ across tenants comes from two already-shipped layers: each GitHub App installation has its own
+ REST budget, and `installation-concurrency-admission.ts` (#2970) caps per-install background
+ GitHub fetches. Further re-tuning still benefits from live `audit_events` / dashboard volume data
+ when available — these defaults are the architecture-justified baseline after the #4903–#4913
+ scaling work, not a production-volume fit.
**Takeaways:**
diff --git a/apps/loopover-ui/content/docs/federated-fleet-intelligence.mdx b/apps/loopover-ui/content/docs/federated-fleet-intelligence.mdx
index 6df9453f91..e6d0926935 100644
--- a/apps/loopover-ui/content/docs/federated-fleet-intelligence.mdx
+++ b/apps/loopover-ui/content/docs/federated-fleet-intelligence.mdx
@@ -1,6 +1,7 @@
---
title: Federated fleet intelligence
description: Opt-in sharing of anonymized gate-calibration aggregates between self-hosted LoopOver instances — what is shared, how consent works, and how trust is gated.
+eyebrow: Self-hosting
---
Federated fleet intelligence is an **opt-in** path for self-hosted LoopOver (ORB) operators who want to compare their own gate calibration against peer medians. Nothing is shared unless you turn it on in config-as-code. There is no default collector and no automatic peer discovery.
@@ -15,20 +16,20 @@ Every field in the bundle body is aggregate-only. The export query never selects
Fields currently enumerated on the signed body (`FederatedSignalBundleBody`):
-| Field | Meaning |
-| --- | --- |
-| `schemaVersion` | Bundle contract version (receivers can reject unknown versions) |
-| `instanceId` | Opaque per-instance handle (no PII) |
-| `generatedAt` | ISO timestamp the bundle was built |
-| `windowDays` | Calibration window length (so peers only median equal-length windows) |
-| `decided` | Resolved PRs in-window that the gate decided |
-| `mergePrecision` | P(merged & not reverted \| gate said merge), or null until enough samples |
-| `closePrecision` | P(closed & not reopened \| gate said close), or null until enough samples |
-| `fpRate` / `fnRate` | False-positive / false-negative rates, or null until eligible |
-| `reversalRate` | Share of decided PRs a human reversed |
-| `cycleP50Ms` / `cycleP95Ms` | Gate-decision → resolution latency percentiles |
-| `slopRate` | Share of decided PRs whose reason bucketed to `slop_advisory` |
-| `copycatRate` | Share whose reason bucketed to `duplicate_risk` (aggregate rate only) |
+| Field | Meaning |
+| --------------------------- | ------------------------------------------------------------------------- |
+| `schemaVersion` | Bundle contract version (receivers can reject unknown versions) |
+| `instanceId` | Opaque per-instance handle (no PII) |
+| `generatedAt` | ISO timestamp the bundle was built |
+| `windowDays` | Calibration window length (so peers only median equal-length windows) |
+| `decided` | Resolved PRs in-window that the gate decided |
+| `mergePrecision` | P(merged & not reverted \| gate said merge), or null until enough samples |
+| `closePrecision` | P(closed & not reopened \| gate said close), or null until enough samples |
+| `fpRate` / `fnRate` | False-positive / false-negative rates, or null until eligible |
+| `reversalRate` | Share of decided PRs a human reversed |
+| `cycleP50Ms` / `cycleP95Ms` | Gate-decision → resolution latency percentiles |
+| `slopRate` | Share of decided PRs whose reason bucketed to `slop_advisory` |
+| `copycatRate` | Share whose reason bucketed to `duplicate_risk` (aggregate rate only) |
A detached `signature` accompanies the body. Export today signs with a local HMAC key; the trust design (#6477) calls for **operator-allowlisted peer public keys** on the receive path so verification does not require sharing an anonymization secret.
@@ -64,9 +65,9 @@ To also arm the **transport client** (push this instance's bundle and/or pull pe
The trust model (#6477) is intentional and checkable — not a reputation system:
1. **Explicit allowlist, not discovery.** Peer trust is operator-configured: you only consider bundles from sources you deliberately configured (a peer public key on your allowlist, or a collector URL you chose). There is no automatic peer discovery and no central PKI.
-2. **Collector mode is the same rule.** Pointing at a collector *is* the trust decision. The client does not add a second peer-vetting layer on top of that choice.
+2. **Collector mode is the same rule.** Pointing at a collector _is_ the trust decision. The client does not add a second peer-vetting layer on top of that choice.
3. **Median, not mean.** Peer comparisons use a **median** of peer calibration metrics. A median resists a bounded number of outlier/poisoned bundles; a mean does not.
-4. **Sybil cost is on the receiver.** Flooding fake peers only matters if *you* added many fake keys to your own allowlist — that is self-limiting by construction.
+4. **Sybil cost is on the receiver.** Flooding fake peers only matters if _you_ added many fake keys to your own allowlist — that is self-limiting by construction.
5. **Abuse recovery.** If a peer looks wrong, remove that key (or stop pointing at that collector). v1 does not ship a separate retroactive-detection subsystem.
Until #6480's import/validation surface is live in your deployment, do not treat pulled files as automatically trusted calibration input.
diff --git a/apps/loopover-ui/content/docs/github-app.mdx b/apps/loopover-ui/content/docs/github-app.mdx
index c318da37da..ae9e5821b2 100644
--- a/apps/loopover-ui/content/docs/github-app.mdx
+++ b/apps/loopover-ui/content/docs/github-app.mdx
@@ -1,6 +1,7 @@
---
title: GitHub App configuration
description: Install a LoopOver GitHub App on a repo so it reviews your pull requests, then choose whether it should stay advisory or enforce repo-configured PR quality rules.
+eyebrow: Workflows
---
Once installed, a **LoopOver GitHub App reviews every pull request** on the repos you select.
@@ -155,17 +156,19 @@ settings:
publicSurface: comment_only
# Friendly gate alias (wins over settings: for gate fields):
+
gate:
- enabled: true # review-agent check on/off
- linkedIssue: advisory # block | advisory | off
- duplicates: block
- readiness: { mode: advisory, minScore: 60 }
+enabled: true # review-agent check on/off
+linkedIssue: advisory # block | advisory | off
+duplicates: block
+readiness: { mode: advisory, minScore: 60 }
# Public review-panel content:
+
review:
- footer: { text: "Reviewed by our bot." } # custom lead — the Gittensor register link is always appended
- note: "Run npm test before requesting review."
- fields: { relatedWork: false } # show/hide individual panel rows`}
+footer: { text: "Reviewed by our bot." } # custom lead — the Gittensor register link is always appended
+note: "Run npm test before requesting review."
+fields: { relatedWork: false } # show/hide individual panel rows`}
/>
Maintainer-supplied footer and note text is dropped if it contains forbidden public language
diff --git a/apps/loopover-ui/content/docs/how-reviews-work.mdx b/apps/loopover-ui/content/docs/how-reviews-work.mdx
index ba11c2d0c2..0c55855e8c 100644
--- a/apps/loopover-ui/content/docs/how-reviews-work.mdx
+++ b/apps/loopover-ui/content/docs/how-reviews-work.mdx
@@ -1,6 +1,7 @@
---
title: How reviews work
description: What LoopOver does when a pull request opens — the gate, the dual-AI review and consensus, and the single comment that surfaces it.
+eyebrow: Reviews
---
## The shape of a review
diff --git a/apps/loopover-ui/content/docs/loopover-commands.mdx b/apps/loopover-ui/content/docs/loopover-commands.mdx
index a30ff6d566..0097d48a99 100644
--- a/apps/loopover-ui/content/docs/loopover-commands.mdx
+++ b/apps/loopover-ui/content/docs/loopover-commands.mdx
@@ -1,6 +1,7 @@
---
title: "@loopover command reference"
description: The full PR and issue-comment control surface -- Q&A verbs, maintainer digests, and PR action commands. Roles reflect the shipped default policy; maintainers can override per command in .loopover.yml.
+eyebrow: Commands
---
import {
diff --git a/apps/loopover-ui/content/docs/maintainer-install-trust.mdx b/apps/loopover-ui/content/docs/maintainer-install-trust.mdx
index e9f2138a8a..8dba6c6297 100644
--- a/apps/loopover-ui/content/docs/maintainer-install-trust.mdx
+++ b/apps/loopover-ui/content/docs/maintainer-install-trust.mdx
@@ -1,6 +1,7 @@
---
title: Maintainer install and trust guide
description: A maintainer-first checklist for self-hosting and installing a GitHub App, keeping public output safe, authorizing commands, using the browser extension, and rejecting weak LoopOver-driven PRs.
+eyebrow: Launch guide
---
import { MAINTAINER_COMMAND_LIST, PUBLIC_COMMAND_LIST } from "@/lib/command-reference";
diff --git a/apps/loopover-ui/content/docs/maintainer-self-hosting.mdx b/apps/loopover-ui/content/docs/maintainer-self-hosting.mdx
index 3c4d7c39fa..7e39389395 100644
--- a/apps/loopover-ui/content/docs/maintainer-self-hosting.mdx
+++ b/apps/loopover-ui/content/docs/maintainer-self-hosting.mdx
@@ -1,6 +1,7 @@
---
title: Self-hosted reviews
description: Run the LoopOver review service on your own infrastructure, with your own data store, GitHub App, AI provider, enrichment service, observability, and private repo policy.
+eyebrow: Maintainers
---
diff --git a/apps/loopover-ui/content/docs/maintainer-workflow.mdx b/apps/loopover-ui/content/docs/maintainer-workflow.mdx
index 482db7f751..244f7a36d4 100644
--- a/apps/loopover-ui/content/docs/maintainer-workflow.mdx
+++ b/apps/loopover-ui/content/docs/maintainer-workflow.mdx
@@ -1,6 +1,7 @@
---
title: Maintainer workflow
description: The default posture is silence. You opt into context. The repo stays calm.
+eyebrow: Workflows
---
import { MAINTAINER_COMMAND_LIST, PUBLIC_COMMAND_LIST } from "@/lib/command-reference";
diff --git a/apps/loopover-ui/content/docs/mcp-clients.mdx b/apps/loopover-ui/content/docs/mcp-clients.mdx
index 8a4b08b28f..7a7259815d 100644
--- a/apps/loopover-ui/content/docs/mcp-clients.mdx
+++ b/apps/loopover-ui/content/docs/mcp-clients.mdx
@@ -1,6 +1,7 @@
---
title: MCP client setup
description: Configure your coding agent to talk to the LoopOver MCP. Pick stdio for local agents, remote for cloud agents.
+eyebrow: Get started
---
## Generate config
diff --git a/apps/loopover-ui/content/docs/miner-coding-agent.mdx b/apps/loopover-ui/content/docs/miner-coding-agent.mdx
index 1f14e9b9b4..8a9fccd434 100644
--- a/apps/loopover-ui/content/docs/miner-coding-agent.mdx
+++ b/apps/loopover-ui/content/docs/miner-coding-agent.mdx
@@ -1,6 +1,7 @@
---
title: Miner coding-agent driver
description: Choose a production provider, override the right model and timeout knobs, and recognize credential failures before you chase the wrong layer.
+eyebrow: Configuration
---
The miner resolves `MINER_CODING_AGENT_PROVIDER` as a comma-separated preference list. The first
@@ -47,6 +48,7 @@ MINER_CODING_AGENT_CLAUDE_MODEL=
MINER_CODING_AGENT_TIMEOUT_MS=120000
# Prefer Codex, fall back to Claude.
+
MINER_CODING_AGENT_PROVIDER=codex-cli,claude-cli
MINER_CODING_AGENT_CODEX_MODEL=`}
/>
diff --git a/apps/loopover-ui/content/docs/miner-quickstart.mdx b/apps/loopover-ui/content/docs/miner-quickstart.mdx
index 6cb36e4ef2..acf0e7fcde 100644
--- a/apps/loopover-ui/content/docs/miner-quickstart.mdx
+++ b/apps/loopover-ui/content/docs/miner-quickstart.mdx
@@ -1,6 +1,7 @@
---
title: Miner quickstart by contribution lane
description: Choose how you want to contribute, then follow the same loop — install, sign in, doctor, plan, preflight, packet — with the flags that fit your lane. About five minutes per lane.
+eyebrow: Get started
---
LoopOver is copilot-only. It ranks and explains your options and drafts public-safe PR packets. It
@@ -24,11 +25,13 @@ npx -y @loopover/mcp@latest --help
npm i -g @loopover/mcp@latest
# sign in, confirm identity, check the session
+
loopover-mcp login
loopover-mcp whoami --json
loopover-mcp status --json
# verify API, auth, and the local scorer before any analysis
+
loopover-mcp doctor --json`}
/>
diff --git a/apps/loopover-ui/content/docs/miner-workflow.mdx b/apps/loopover-ui/content/docs/miner-workflow.mdx
index 48ce4e4fca..7733fe1118 100644
--- a/apps/loopover-ui/content/docs/miner-workflow.mdx
+++ b/apps/loopover-ui/content/docs/miner-workflow.mdx
@@ -1,6 +1,7 @@
---
title: Miner workflow
description: A deterministic four-step loop. Each step is pure metadata; each output is structured JSON your agent can consume.
+eyebrow: Workflows
---
If the workflow will spawn Claude Code or Codex, configure that driver first in [Miner
diff --git a/apps/loopover-ui/content/docs/owner-checklist.mdx b/apps/loopover-ui/content/docs/owner-checklist.mdx
index 9cb6d157fe..354de7d615 100644
--- a/apps/loopover-ui/content/docs/owner-checklist.mdx
+++ b/apps/loopover-ui/content/docs/owner-checklist.mdx
@@ -1,6 +1,7 @@
---
title: Repo-owner onboarding checklist
description: Work through this before you invite LoopOver contribution traffic. It mirrors the readiness report exactly, so each item is something the platform actually checks -- and each comes with the honest tradeoff you are opting into.
+eyebrow: Repo owners
---
LoopOver is **quiet by default**: it installs without posting comments or
diff --git a/apps/loopover-ui/content/docs/privacy-security.mdx b/apps/loopover-ui/content/docs/privacy-security.mdx
index e634948bce..4332364d28 100644
--- a/apps/loopover-ui/content/docs/privacy-security.mdx
+++ b/apps/loopover-ui/content/docs/privacy-security.mdx
@@ -1,6 +1,7 @@
---
title: Privacy & security
description: Privacy is the product. These are hard rules, not best-effort goals.
+eyebrow: Operating
---
## Hard rules
@@ -61,12 +62,13 @@ LOOPOVER_REVIEW_IMPROVEMENT_SIGNAL="true" # read-only PR quality-delta sign
LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE="true" # upgrade-only AMS track-record bridge (never shown; needs features.amsReputationBridge + LOOPOVER_AMS_TRACK_RECORD_URL)
# Global (cron / endpoint) flags, not scoped by LOOPOVER_REVIEW_REPOS.
-LOOPOVER_REVIEW_CONTINUOUS="true" # fleet-wide default: re-review on every push (else one-shot)
-LOOPOVER_REVIEW_OPS="true" # read-only anomaly scan + outcome stats endpoint
-LOOPOVER_REVIEW_SELFTUNE="true" # self-tightening tuning loop, never loosens
-LOOPOVER_REVIEW_PARITY_AUDIT="true" # shadow-record gate-decision parity readiness
-LOOPOVER_REVIEW_CONTENT_LANE="true" # dedicated content/registry-repo review lane
-LOOPOVER_REVIEW_DRAFT="true" # public draft-submission (contributor fork PR) flow`}
+
+LOOPOVER_REVIEW_CONTINUOUS="true" # fleet-wide default: re-review on every push (else one-shot)
+LOOPOVER_REVIEW_OPS="true" # read-only anomaly scan + outcome stats endpoint
+LOOPOVER_REVIEW_SELFTUNE="true" # self-tightening tuning loop, never loosens
+LOOPOVER_REVIEW_PARITY_AUDIT="true" # shadow-record gate-decision parity readiness
+LOOPOVER_REVIEW_CONTENT_LANE="true" # dedicated content/registry-repo review lane
+LOOPOVER_REVIEW_DRAFT="true" # public draft-submission (contributor fork PR) flow`}
/>
The internal-only controls never surface publicly. Submitter reputation, for example, can
@@ -102,7 +104,7 @@ anti-abuse defaults that reveal no review direction and are not per-repo tunable
## Orb fleet telemetry (#4893)
The always-on fleet-calibration pipeline (`POST /v1/orb/ingest`) aggregates anonymized signals
-from every registered *and unregistered* self-host instance into one shared table
+from every registered _and unregistered_ self-host instance into one shared table
(`orb_signals`) — see [self-hosting: GitHub App](/docs/self-hosting-github-app#telemetry-is-separate-from-token-brokerage)
for what's exported today and how to opt out. That table is intentional and not a leak: repo/PR
identifiers are HMAC-anonymized with a per-instance secret LoopOver's own collector never holds,
@@ -114,24 +116,23 @@ grows a hosted, per-customer model, what would be safe to add to this pipeline,
wouldn't?** A clear yes/no per category, so a future PR that touches this table has a rule to
check itself against instead of relitigating anonymization from scratch.
-| Potential future data | Safe to add? | Why |
-| --- | --- | --- |
-| Raw repo/org names or logins, even opt-in | **No** | The anonymization promise is fleet-wide, not per-tenant-configurable — one customer opting into raw identifiers doesn't change what every *other* instance was told about this shared table. A hosted customer who wants their own data identifiable should read it back from their own instance/dashboard, never from the shared fleet table. |
-| PR/commit content, diffs, or comments | **No** | Never queried into the export today (the collector's own DB query selects only verdict/outcome/reversal/timestamps) — there is no path for this to leave an instance short of a deliberate, separate feature. Adding it to fleet telemetry would be a new capability, not an extension of this one. |
-| Full-text gate reasons | **No** | Already bucketed to a fixed, low-cardinality category before export specifically so free text (which can quote file paths, identifiers, or reviewer commentary) never leaves the instance. A hosted model doesn't change the fact that this text wasn't written for cross-tenant eyes. |
-| A raw `installation_id` column on `orb_signals` itself | **No** | `orb_signals`/`orb_instances` were built with only an opaque, HMAC-derived `instance_id` — retrofitting a raw installation id onto that table would retroactively de-anonymize every row collected under the existing anonymity promise. If hosted billing/support ever needs to map a fleet instance to a paying installation, that mapping belongs in a **separate** table (mirroring how `orb_github_installations` already keeps raw account identity in its own table, entirely apart from `orb_signals`), not a new column here. |
-| Per-instance metrics surfaced back to *that instance's own* operator | **Yes, with new authz** | `GET /v1/internal/fleet/analytics` already computes per-instance breakdowns keyed by the same opaque `instanceId` — the data already exists. A hosted per-customer dashboard could show a customer their own row. What doesn't exist yet is anything that maps "this session belongs to instance X" and enforces that a customer can only ever see their own opaque id, never another tenant's — that authz layer is new work, not a data-model change. |
-| Cross-tenant fleet-median/percentile comparisons (e.g. gate precision vs. the peer median) | **Yes, unchanged** | Already shipped, in aggregate form only — a single percentile/median across the whole fleet, with no way to reverse-engineer any other specific tenant's raw value from it. This category is the one place "more data" is already safe, because it was designed to stay aggregate-only from the start. |
+| Potential future data | Safe to add? | Why |
+| ------------------------------------------------------------------------------------------ | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Raw repo/org names or logins, even opt-in | **No** | The anonymization promise is fleet-wide, not per-tenant-configurable — one customer opting into raw identifiers doesn't change what every _other_ instance was told about this shared table. A hosted customer who wants their own data identifiable should read it back from their own instance/dashboard, never from the shared fleet table. |
+| PR/commit content, diffs, or comments | **No** | Never queried into the export today (the collector's own DB query selects only verdict/outcome/reversal/timestamps) — there is no path for this to leave an instance short of a deliberate, separate feature. Adding it to fleet telemetry would be a new capability, not an extension of this one. |
+| Full-text gate reasons | **No** | Already bucketed to a fixed, low-cardinality category before export specifically so free text (which can quote file paths, identifiers, or reviewer commentary) never leaves the instance. A hosted model doesn't change the fact that this text wasn't written for cross-tenant eyes. |
+| A raw `installation_id` column on `orb_signals` itself | **No** | `orb_signals`/`orb_instances` were built with only an opaque, HMAC-derived `instance_id` — retrofitting a raw installation id onto that table would retroactively de-anonymize every row collected under the existing anonymity promise. If hosted billing/support ever needs to map a fleet instance to a paying installation, that mapping belongs in a **separate** table (mirroring how `orb_github_installations` already keeps raw account identity in its own table, entirely apart from `orb_signals`), not a new column here. |
+| Per-instance metrics surfaced back to _that instance's own_ operator | **Yes, with new authz** | `GET /v1/internal/fleet/analytics` already computes per-instance breakdowns keyed by the same opaque `instanceId` — the data already exists. A hosted per-customer dashboard could show a customer their own row. What doesn't exist yet is anything that maps "this session belongs to instance X" and enforces that a customer can only ever see their own opaque id, never another tenant's — that authz layer is new work, not a data-model change. |
+| Cross-tenant fleet-median/percentile comparisons (e.g. gate precision vs. the peer median) | **Yes, unchanged** | Already shipped, in aggregate form only — a single percentile/median across the whole fleet, with no way to reverse-engineer any other specific tenant's raw value from it. This category is the one place "more data" is already safe, because it was designed to stay aggregate-only from the start. |
Anonymization is enforced entirely on the **sender**, not the receiver: `ORB_ANONYMIZE=false`
- makes a self-host instance export raw repo/PR names, and `POST /v1/orb/ingest` has no
- server-side check that a submitted `repo_hash`/`pr_hash` actually looks like a hash before
- storing it in the shared table — it validates only type and length. Today this only affects an
- operator who deliberately opted out for their own private collector. It becomes a sharper
- problem the moment `/v1/orb/ingest` is fronted by anything resembling a hosted, multi-tenant
- path: a misconfigured or malicious sender could land raw identifiers in a table every other
- instance was told stays anonymized. Closing this (reject a submission whose `repo_hash`/`pr_hash`
- doesn't match the expected HMAC-output shape) is real follow-up work, tracked separately from
- this review.
+ makes a self-host instance export raw repo/PR names, and `POST /v1/orb/ingest` has no server-side
+ check that a submitted `repo_hash`/`pr_hash` actually looks like a hash before storing it in the
+ shared table — it validates only type and length. Today this only affects an operator who
+ deliberately opted out for their own private collector. It becomes a sharper problem the moment
+ `/v1/orb/ingest` is fronted by anything resembling a hosted, multi-tenant path: a misconfigured or
+ malicious sender could land raw identifiers in a table every other instance was told stays
+ anonymized. Closing this (reject a submission whose `repo_hash`/`pr_hash` doesn't match the
+ expected HMAC-output shape) is real follow-up work, tracked separately from this review.
diff --git a/apps/loopover-ui/content/docs/quickstart.mdx b/apps/loopover-ui/content/docs/quickstart.mdx
index 5fe46f9c87..e48f3142ef 100644
--- a/apps/loopover-ui/content/docs/quickstart.mdx
+++ b/apps/loopover-ui/content/docs/quickstart.mdx
@@ -1,6 +1,7 @@
---
title: Quickstart
description: Install the MCP, sign in, and run your first analysis. About two minutes.
+eyebrow: Get started
---
## 1. Install
@@ -13,6 +14,7 @@ install it globally.
npx -y @loopover/mcp@latest --help
# install
+
npm i -g @loopover/mcp@latest`}
/>
diff --git a/apps/loopover-ui/content/docs/scoreability.mdx b/apps/loopover-ui/content/docs/scoreability.mdx
index fa0df76df3..ea0499a306 100644
--- a/apps/loopover-ui/content/docs/scoreability.mdx
+++ b/apps/loopover-ui/content/docs/scoreability.mdx
@@ -1,6 +1,7 @@
---
title: Scoreability
description: LoopOver projects how scoreable your branch is under several scenarios. These are estimates, never guarantees.
+eyebrow: Core concepts
---
## The seven scenarios
diff --git a/apps/loopover-ui/content/docs/self-hosting-ai-providers.mdx b/apps/loopover-ui/content/docs/self-hosting-ai-providers.mdx
index f1cecbb722..d1beb75912 100644
--- a/apps/loopover-ui/content/docs/self-hosting-ai-providers.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-ai-providers.mdx
@@ -1,6 +1,7 @@
---
title: AI providers
description: AI review is optional. The deterministic gate still runs when no provider is configured.
+eyebrow: Self-hosting
---
## Provider options
diff --git a/apps/loopover-ui/content/docs/self-hosting-backup-scaling.mdx b/apps/loopover-ui/content/docs/self-hosting-backup-scaling.mdx
index c943ebaaca..dab078d0a0 100644
--- a/apps/loopover-ui/content/docs/self-hosting-backup-scaling.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-backup-scaling.mdx
@@ -1,6 +1,7 @@
---
title: Backup and scaling
description: Choose the right data layout for one node or many, and make sure the review state can be restored.
+eyebrow: Self-hosting
---
## Default: SQLite single node
diff --git a/apps/loopover-ui/content/docs/self-hosting-configuration.mdx b/apps/loopover-ui/content/docs/self-hosting-configuration.mdx
index a6eb019e28..6fa9a2ef3b 100644
--- a/apps/loopover-ui/content/docs/self-hosting-configuration.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-configuration.mdx
@@ -1,6 +1,7 @@
---
title: Configuration
description: "The self-host configuration model: deployment env, private per-repo policy, feature flags, and review modes."
+eyebrow: Self-hosting
---
import { SELFHOST_ENV_REFERENCE_MARKDOWN } from "@/lib/selfhost-env-reference";
@@ -81,6 +82,7 @@ Start from a template instead of reverse-engineering env flags, private-config p
cp config/examples/loopover.minimal.yml .loopover.yml
# Self-host private mount (operator-only policy)
+
mkdir -p loopover-config
cp config/examples/global.loopover.yml loopover-config/.loopover.yml`}
/>
@@ -366,7 +368,7 @@ A cluster of contributor-abuse guardrails, all config-as-code only, all off/unse
- **Exemptions and account age** — `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.
- **Command rate limit** — `commandRateLimitPolicy` (`off`/`hold`, default `off`) generalizes the review-nag pattern to every `@loopover` 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.
-### settings.issuePlan*
+### settings.issuePlan\*
Config for `loopover_plan_repo_issues`, the AI issue-planning MCP tool: given a maintainer-supplied free-form goal, it proposes a small set of GitHub issue drafts (dry-run by default) and, on explicit request, creates them via the same installation-token/Orb-broker path every other GitHub write in this repo uses — never a flat PAT, so it only works on a repo the App/Orb is actually installed on.
diff --git a/apps/loopover-ui/content/docs/self-hosting-docs-audit.mdx b/apps/loopover-ui/content/docs/self-hosting-docs-audit.mdx
index bab92eef75..feb51302e0 100644
--- a/apps/loopover-ui/content/docs/self-hosting-docs-audit.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-docs-audit.mdx
@@ -1,6 +1,7 @@
---
title: Docs accuracy audit
description: Before a self-host release, use this checklist to confirm website docs still match runtime defaults. Each row links a docs page to the files and CI drift guards that must stay aligned.
+eyebrow: Self-hosting
---
import { Link } from "@tanstack/react-router";
diff --git a/apps/loopover-ui/content/docs/self-hosting-github-app.mdx b/apps/loopover-ui/content/docs/self-hosting-github-app.mdx
index 0bffb9749c..0a04dc949c 100644
--- a/apps/loopover-ui/content/docs/self-hosting-github-app.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-github-app.mdx
@@ -1,6 +1,7 @@
---
title: GitHub App and Orb
description: A self-host needs webhook delivery and installation tokens. Direct GitHub App is the default, recommended model -- Orb broker mode is private/managed-beta only.
+eyebrow: Self-hosting
---
## Choose a connection mode
diff --git a/apps/loopover-ui/content/docs/self-hosting-operations.mdx b/apps/loopover-ui/content/docs/self-hosting-operations.mdx
index 5a653282ad..e1fdcb96a9 100644
--- a/apps/loopover-ui/content/docs/self-hosting-operations.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-operations.mdx
@@ -1,6 +1,7 @@
---
title: Operations
description: "Daily operating checks for the review service: health, queue, logs, metrics, dashboards, and context services."
+eyebrow: Self-hosting
---
## Health endpoints
@@ -494,31 +495,29 @@ repeat the extension block in your override file:
TEMP: /tmp/runner/tmp
services:
- runner-2-tmp-init:
- image: alpine:3.20
- profiles: ["runners"]
- volumes:
- - runner-work-2:/tmp/runner
- command: ["sh", "-c", "mkdir -p /tmp/runner/tmp && chmod 1777 /tmp/runner/tmp"]
-
- runner-2:
- image: myoung34/github-runner:ubuntu-jammy
- profiles: ["runners"]
- depends_on:
- runner-2-tmp-init:
- condition: service_completed_successfully
- environment:
- <<: *runner-tmp-env
- RUNNER_NAME: loopover-runner-2
- RUNNER_SCOPE: \${RUNNER_SCOPE:-repo}
- REPO_URL: \${RUNNER_REPO_URL:-}
- RUNNER_TOKEN: \${RUNNER_TOKEN:-}
- RUNNER_WORKDIR: /tmp/runner
- volumes:
- - runner-work-2:/tmp/runner
+runner-2-tmp-init:
+image: alpine:3.20
+profiles: ["runners"]
+volumes: - runner-work-2:/tmp/runner
+command: ["sh", "-c", "mkdir -p /tmp/runner/tmp && chmod 1777 /tmp/runner/tmp"]
+
+runner-2:
+image: myoung34/github-runner:ubuntu-jammy
+profiles: ["runners"]
+depends_on:
+runner-2-tmp-init:
+condition: service_completed_successfully
+environment:
+<<: \*runner-tmp-env
+RUNNER_NAME: loopover-runner-2
+RUNNER_SCOPE: \${RUNNER_SCOPE:-repo}
+REPO_URL: \${RUNNER_REPO_URL:-}
+RUNNER_TOKEN: \${RUNNER_TOKEN:-}
+RUNNER_WORKDIR: /tmp/runner
+volumes: - runner-work-2:/tmp/runner
volumes:
- runner-work-2:`}
+runner-work-2:`}
/>
## Enabling Sentry (your own DSN)
@@ -973,6 +972,7 @@ then persists the resolved image reference back to `LOOPOVER_IMAGE` in
./scripts/deploy-selfhost-image.sh
# Pin an exact release tag or content digest
+
./scripts/deploy-selfhost-image.sh ghcr.io/jsonbored/loopover-selfhost:orb-v0.1.0
LOOPOVER_IMAGE=ghcr.io/jsonbored/loopover-selfhost@sha256:... ./scripts/deploy-selfhost-image.sh`}
/>
@@ -1017,7 +1017,9 @@ script assumes that checkout already exists and already tracks `origin/main`.
SELFHOST_UPDATE_REMOTE=upstream SELFHOST_UPDATE_BRANCH=main ./scripts/selfhost-update.sh
# Skip the health probe step (e.g. you'll run it yourself, or right after a schema-changing release
+
# where you want to inspect logs before curling /ready)
+
SELFHOST_SKIP_POST_UPDATE_CHECK=1 ./scripts/selfhost-update.sh`}
/>
@@ -1112,8 +1114,9 @@ observation window by design (3 minutes by default):
code={`./scripts/selfhost-post-update-regression-gate.sh
# Tune the observation window and/or the dead-job threshold that triggers the pause
+
SELFHOST_REGRESSION_WINDOW_SECONDS=300 SELFHOST_REGRESSION_JOB_DEAD_THRESHOLD=10 \\
- ./scripts/selfhost-post-update-regression-gate.sh`}
+./scripts/selfhost-post-update-regression-gate.sh`}
/>
If it trips, clear the pause once you've confirmed the regression is fixed:
diff --git a/apps/loopover-ui/content/docs/self-hosting-quickstart.mdx b/apps/loopover-ui/content/docs/self-hosting-quickstart.mdx
index 7cfc484a23..d9b68f4eec 100644
--- a/apps/loopover-ui/content/docs/self-hosting-quickstart.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-quickstart.mdx
@@ -1,6 +1,7 @@
---
title: Quickstart
description: A minimal self-host boot path for maintainers -- start the service, verify readiness, and keep the first rollout safe.
+eyebrow: Self-hosting
---
## 1. Copy the sample env
@@ -240,19 +241,19 @@ Nothing below needs a flag to start; everything past the first row needs an expl
loopover app + Redis SQLite database, dry-run-friendly, Orb telemetry (see Callout below)
RECOMMENDED FOR PRODUCTION (opt-in)
- --profile postgres shared/multi-instance database (pgvector-capable)
- --profile pgbouncer connection pooling in front of Postgres
- --profile caddy automatic HTTPS via Let's Encrypt
- --profile litestream continuous SQLite backup to S3-compatible storage
- --profile observability Prometheus + Alertmanager + Loki + Grafana
+--profile postgres shared/multi-instance database (pgvector-capable)
+--profile pgbouncer connection pooling in front of Postgres
+--profile caddy automatic HTTPS via Let's Encrypt
+--profile litestream continuous SQLite backup to S3-compatible storage
+--profile observability Prometheus + Alertmanager + Loki + Grafana
OPT-IN, NOT REQUIRED FOR A TRIAL INSTANCE
- --profile qdrant dedicated RAG vector store (else sqlite-vec/pgvector)
- --profile ollama local model for AI review or embeddings
- --profile tailscale private network sidecar
- --profile runners self-hosted GitHub Actions runner
- --profile backup scheduled backup + backup-exporter jobs
- AI_PROVIDER=... off by default; reviews are deterministic-only until set`} />
+--profile qdrant dedicated RAG vector store (else sqlite-vec/pgvector)
+--profile ollama local model for AI review or embeddings
+--profile tailscale private network sidecar
+--profile runners self-hosted GitHub Actions runner
+--profile backup scheduled backup + backup-exporter jobs
+AI_PROVIDER=... off by default; reviews are deterministic-only until set`} />
Orb fleet-calibration telemetry (verdict, outcome, cycle time — never repo names, code, or logins)
diff --git a/apps/loopover-ui/content/docs/self-hosting-rag.mdx b/apps/loopover-ui/content/docs/self-hosting-rag.mdx
index 800dfb131b..b80ab989f2 100644
--- a/apps/loopover-ui/content/docs/self-hosting-rag.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-rag.mdx
@@ -1,6 +1,7 @@
---
title: RAG indexing
description: RAG adds relevant existing code and docs to the AI reviewer prompt. It is additive and fail-safe.
+eyebrow: Self-hosting
---
## Prerequisites
diff --git a/apps/loopover-ui/content/docs/self-hosting-rees-analyzers.mdx b/apps/loopover-ui/content/docs/self-hosting-rees-analyzers.mdx
index fc08757e92..e6e9bdbda3 100644
--- a/apps/loopover-ui/content/docs/self-hosting-rees-analyzers.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-rees-analyzers.mdx
@@ -1,6 +1,7 @@
---
title: REES analyzer reference
description: Every analyzer name you can put in REES_ANALYZERS, what it inspects, what it reports, and whether it needs network or GitHub token access.
+eyebrow: Self-hosting
---
import { REES_ANALYZERS, REES_ANALYZER_NAMES, REES_PROFILES } from "@/lib/rees-analyzers";
@@ -17,9 +18,11 @@ local and low-cost registry checks.
REES_ANALYZERS=all
# A subset runs only the named analyzers.
+
REES_ANALYZERS=secret,actionPin,redos
# Invalid names are ignored; if none are valid, REES runs no analyzers.
+
REES_ANALYZERS=unknownName`}
/>
diff --git a/apps/loopover-ui/content/docs/self-hosting-rees.mdx b/apps/loopover-ui/content/docs/self-hosting-rees.mdx
index 9df7cdad0e..52a89b2277 100644
--- a/apps/loopover-ui/content/docs/self-hosting-rees.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-rees.mdx
@@ -1,6 +1,7 @@
---
title: REES enrichment
description: REES runs external or heavier analyzers and returns a public-safe brief that the AI reviewer can use.
+eyebrow: Self-hosting
---
import { REES_ANALYZER_NAMES } from "@/lib/rees-analyzers";
@@ -137,6 +138,7 @@ forwarding stays off unless you explicitly set `REES_FORWARD_GITHUB_TOKEN=true`.
LOOPOVER_REVIEW_ENRICHMENT=false
# Keep REES on and explicitly allow token-aware analyzers:
+
REES_FORWARD_GITHUB_TOKEN=true`}
/>
diff --git a/apps/loopover-ui/content/docs/self-hosting-release-checklist.mdx b/apps/loopover-ui/content/docs/self-hosting-release-checklist.mdx
index 91e105e25b..e3cae445c6 100644
--- a/apps/loopover-ui/content/docs/self-hosting-release-checklist.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-release-checklist.mdx
@@ -1,6 +1,7 @@
---
title: First release checklist
description: Everything to confirm before cutting orb-v0.1.0 — the first stable (non-prerelease) self-host image — after two beta cuts already validated the pipeline end to end. Run the smoke matrix against a candidate image before tagging any orb-vX.Y.Z or -rc/-beta prerelease; CI only exercises the plain SQLite + Redis + direct-App default.
+eyebrow: Self-hosting
---
## Versioning and release trigger
@@ -281,7 +282,9 @@ SELFHOST_SMOKE_FORBID_EVENTS="selfhost_orb_relay_register_failed" \\
./scripts/smoke-selfhost.sh loopover:rc-candidate
# Pull mode -- no PUBLIC_API_ORIGIN needed; the container polls the broker outbound instead of
+
# exposing an inbound endpoint. The right fit for NAT/tailnet operators with no public ingress.
+
SELFHOST_SMOKE_EXTRA_ENV="ORB_ENROLLMENT_SECRET=\${TEST_ENROLLMENT_SECRET}
ORB_RELAY_MODE=pull" \\
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_orb_relay_register" \\
@@ -314,6 +317,7 @@ SELFHOST_SMOKE_FORBID_EVENTS="selfhost_ai_cli_missing" \\
./scripts/smoke-selfhost.sh loopover:rc-candidate
# Codex only (requires the fail-closed opt-in)
+
SELFHOST_SMOKE_EXTRA_ENV="AI_PROVIDER=codex
LOOPOVER_ENABLE_UNSAFE_CODEX_REVIEWER=1" \\
SELFHOST_SMOKE_EXPECT_EVENTS="selfhost_ai_provider" \\
@@ -321,6 +325,7 @@ SELFHOST_SMOKE_FORBID_EVENTS="selfhost_ai_cli_missing" \\
./scripts/smoke-selfhost.sh loopover:rc-candidate
# Codex primary, Claude Code fallback
+
SELFHOST_SMOKE_EXTRA_ENV="AI_PROVIDER=codex,claude-code
CODEX_AI_EFFORT=medium
CLAUDE_AI_EFFORT=medium
@@ -367,6 +372,7 @@ Redis is always-on in every scenario above (the base script already boots it)
./scripts/smoke-selfhost.sh loopover:rc-candidate
# With Qdrant RAG:
+
docker network create gt-rag-smoke
docker run -d --name gt-qdrant --network gt-rag-smoke qdrant/qdrant:v1.18.2
SELFHOST_SMOKE_NETWORK=gt-rag-smoke \\
diff --git a/apps/loopover-ui/content/docs/self-hosting-releases.mdx b/apps/loopover-ui/content/docs/self-hosting-releases.mdx
index 2b30e49426..c4e6e22765 100644
--- a/apps/loopover-ui/content/docs/self-hosting-releases.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-releases.mdx
@@ -1,6 +1,7 @@
---
title: Releases and images
description: How to consume official self-host images, pin versions, build custom images, and keep source maps aligned.
+eyebrow: Self-hosting
---
## Image tags
@@ -70,6 +71,7 @@ A tag like `orb-v0.1.0-rc.1` or `orb-v0.1.0-beta.1` runs the identical build/pro
curl http://localhost:8787/ready
# Building from the current checkout instead of pulling
+
./scripts/deploy-selfhost-prebuilt.sh`}
/>
diff --git a/apps/loopover-ui/content/docs/self-hosting-security.mdx b/apps/loopover-ui/content/docs/self-hosting-security.mdx
index 2cd300a92a..86eaca2828 100644
--- a/apps/loopover-ui/content/docs/self-hosting-security.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-security.mdx
@@ -1,6 +1,7 @@
---
title: Security
description: The self-host stack holds maintainer credentials and policy. Keep those boundaries explicit.
+eyebrow: Self-hosting
---
## Secret handling
@@ -20,7 +21,7 @@ description: The self-host stack holds maintainer credentials and policy. Keep t
{
title: "Rotate deliberately",
description:
- "Cadence, per-secret procedure, and the categories that can't be rotated safely yet -- see \"Secret rotation policy\" below.",
+ 'Cadence, per-secret procedure, and the categories that can\'t be rotated safely yet -- see "Secret rotation policy" below.',
},
]}
/>
@@ -71,20 +72,23 @@ rm secrets/loopover_api_token.txt && ./scripts/selfhost-init-secrets.sh # gene
docker compose up -d --no-deps loopover
# Freely rotatable secret -- hosted Worker path
+
wrangler secret put LOOPOVER_API_TOKEN
+
# then redeploy`}
+
/>
`TOKEN_ENCRYPTION_SECRET` (maintainer BYOK AI-provider keys at rest) and
`DRAFT_TOKEN_ENCRYPTION_SECRET` (in-flight contributor draft OAuth tokens) are AES-256-GCM keys
- every already-encrypted row was written against. The decrypt path always derives the AES key
- from whatever value is currently set, with no fallback to a prior value -- there is no
- re-encryption tooling in this codebase today. Rotating `TOKEN_ENCRYPTION_SECRET` means every
- maintainer who saved a BYOK provider key has to re-enter it; rotating
- `DRAFT_TOKEN_ENCRYPTION_SECRET` fails any draft submission mid-flight. Only rotate these two on
- suspected compromise, and treat the resulting key/token loss as an accepted, unavoidable cost
- until a re-encrypt-in-place migration exists (tracked as separate follow-up work).
+ every already-encrypted row was written against. The decrypt path always derives the AES key from
+ whatever value is currently set, with no fallback to a prior value -- there is no re-encryption
+ tooling in this codebase today. Rotating `TOKEN_ENCRYPTION_SECRET` means every maintainer who
+ saved a BYOK provider key has to re-enter it; rotating `DRAFT_TOKEN_ENCRYPTION_SECRET` fails any
+ draft submission mid-flight. Only rotate these two on suspected compromise, and treat the
+ resulting key/token loss as an accepted, unavoidable cost until a re-encrypt-in-place migration
+ exists (tracked as separate follow-up work).
`ORB_ENROLLMENT_SECRET` has the same gap from the other direction: `orb_enrollments.revoked_at` is
@@ -233,6 +237,7 @@ The shipped `caddy/Caddyfile` reverse-proxies to `loopover:8787` on the compose
output stderr
format json
}
+
}`}
/>
diff --git a/apps/loopover-ui/content/docs/self-hosting-troubleshooting.mdx b/apps/loopover-ui/content/docs/self-hosting-troubleshooting.mdx
index 852c9f369a..48c686085e 100644
--- a/apps/loopover-ui/content/docs/self-hosting-troubleshooting.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-troubleshooting.mdx
@@ -1,6 +1,7 @@
---
title: Troubleshooting
description: Start with readiness and logs, then isolate webhook, queue, AI, REES, RAG, or write-suppression problems.
+eyebrow: Self-hosting
---
## First checks
@@ -87,9 +88,11 @@ A short burst of deferrals is expected and self-resolving: the queue is delibera
sum by (key_scope, job_type) (rate(loopover_jobs_rate_limit_admission_deferred_total[10m]))
# Is one key_scope (e.g. a single installation token) the bottleneck?
+
topk(5, sum by (key_scope) (rate(loopover_jobs_rate_limit_budget_deferred_total[10m])))
# Real rate-limit responses from GitHub itself (not just internal deferrals)
+
sum(rate(loopover_github_rest_rate_limit_responses_total[10m]))`}
/>
@@ -107,6 +110,7 @@ sum by (class) (rate(loopover_github_response_cache_total{result="hit"}[15m]))
sum by (class) (rate(loopover_github_response_cache_total[15m]))
# GraphQL hit rate — same shape, separate metric
+
sum by (class) (rate(loopover_github_graphql_cache_total{result="hit"}[15m]))
/
sum by (class) (rate(loopover_github_graphql_cache_total[15m]))`}
@@ -126,6 +130,7 @@ sum by (class) (rate(loopover_github_graphql_cache_total[15m]))`}
docker compose --profile qdrant ps qdrant
# Only after confirming a dimension mismatch is the actual cause:
+
curl -X DELETE "$QDRANT_URL/collections/loopover"`}
/>
@@ -160,7 +165,9 @@ The metrics path is the app's own `/metrics` (scraped directly by Prometheus) pl
docker compose logs --tail=80 prometheus otel-collector grafana
# Send one synthetic OTLP metric through the collector, read it back from its Prometheus exporter,
+
# and sanity-check the app's own /metrics HELP/TYPE shape.
+
npm run test:smoke:observability:metrics`}
/>
@@ -179,6 +186,7 @@ The trace path is app or smoke process → OTEL collector → Tempo → Grafana.
docker compose logs --tail=80 tempo otel-collector grafana
# Send one synthetic span through the collector and read it back from Tempo.
+
npm run test:smoke:observability`}
/>
diff --git a/apps/loopover-ui/content/docs/self-hosting-unified-ams-orb.mdx b/apps/loopover-ui/content/docs/self-hosting-unified-ams-orb.mdx
index 868175ba2a..f34c24811e 100644
--- a/apps/loopover-ui/content/docs/self-hosting-unified-ams-orb.mdx
+++ b/apps/loopover-ui/content/docs/self-hosting-unified-ams-orb.mdx
@@ -1,6 +1,7 @@
---
title: Running ORB and AMS together
description: Run ORB (self-hosted PR review) and AMS (the autonomous miner) together on one host -- shared state, aligned .env, and working AMS Grafana panels in fleet mode.
+eyebrow: Self-hosting
---
ORB (self-hosted PR review) and AMS (the autonomous miner) each run on their own, but there is one
@@ -67,7 +68,7 @@ two never line up on their own and the Grafana AMS datasources stay **silently e
## 3. Bridge the state so AMS panels populate
The AMS package ships an opt-in override that relocates the fleet miner's `/data/miner` state onto
-the *same* host directory the exporter reads — using the same `LOOPOVER_MINER_CONFIG_DIR`
+the _same_ host directory the exporter reads — using the same `LOOPOVER_MINER_CONFIG_DIR`
variable and default, so there is no `docker volume inspect` archaeology. Copy the example (it is
gitignored) and run all three compose files together with both profiles:
@@ -77,14 +78,14 @@ gitignored) and run all three compose files together with both profiles:
packages/loopover-miner/docker-compose.miner.override.yml # edit the host path only for a non-default location
docker compose \\
- -f docker-compose.yml \\
- -f packages/loopover-miner/docker-compose.miner.yml \\
- -f packages/loopover-miner/docker-compose.miner.override.yml \\
- --profile observability --profile ams-observability up -d`}
+-f docker-compose.yml \\
+-f packages/loopover-miner/docker-compose.miner.yml \\
+-f packages/loopover-miner/docker-compose.miner.override.yml \\
+--profile observability --profile ams-observability up -d`}
/>
The override bind-mounts `/data/miner` to `${LOOPOVER_MINER_CONFIG_DIR:-~/.config/loopover-miner}`
-— Compose merges by container path, so this *replaces* the base file's `miner-data` named-volume
+— Compose merges by container path, so this _replaces_ the base file's `miner-data` named-volume
mount for the same target rather than adding a second one. The exporter's own bind is the same
source: `${LOOPOVER_MINER_CONFIG_DIR:-~/.config/loopover-miner}:/ams-ledgers:ro`. Both sides now
read one location.
diff --git a/apps/loopover-ui/content/docs/troubleshooting.mdx b/apps/loopover-ui/content/docs/troubleshooting.mdx
index 4966690173..5849ba9c48 100644
--- a/apps/loopover-ui/content/docs/troubleshooting.mdx
+++ b/apps/loopover-ui/content/docs/troubleshooting.mdx
@@ -1,6 +1,7 @@
---
title: Troubleshooting
description: The MCP ships with diagnostics. Start with doctor, then drill in.
+eyebrow: Operating
---
## Health checks
@@ -28,6 +29,7 @@ work is happening; traces answer _where time went_ inside a review job.
docker compose --profile observability up -d
# Export app queue-job and AI-provider spans to Tempo.
+
OTEL_TRACES_EXPORTER=otlp
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.05
diff --git a/apps/loopover-ui/content/docs/tuning.mdx b/apps/loopover-ui/content/docs/tuning.mdx
index 6bd9f6b058..e1db382204 100644
--- a/apps/loopover-ui/content/docs/tuning.mdx
+++ b/apps/loopover-ui/content/docs/tuning.mdx
@@ -1,6 +1,7 @@
---
title: Tuning your reviews
description: How to configure LoopOver CI and LoopOver review -- gate modes, score thresholds, guardrails, and feature flags -- through .loopover.yml and your repo settings.
+eyebrow: Operating
---
## How configuration fits together
diff --git a/apps/loopover-ui/content/docs/upstream-drift.mdx b/apps/loopover-ui/content/docs/upstream-drift.mdx
index fa78cd78b4..d49504796a 100644
--- a/apps/loopover-ui/content/docs/upstream-drift.mdx
+++ b/apps/loopover-ui/content/docs/upstream-drift.mdx
@@ -1,6 +1,7 @@
---
title: Upstream drift
description: Gittensor moves. LoopOver tracks every meaningful change to scoring, registry, and issue-discovery so your decisions stay grounded.
+eyebrow: Core concepts
---
## How drift works
diff --git a/apps/loopover-ui/source.config.ts b/apps/loopover-ui/source.config.ts
index 128a5c1754..afb1bc5478 100644
--- a/apps/loopover-ui/source.config.ts
+++ b/apps/loopover-ui/source.config.ts
@@ -1,5 +1,18 @@
import { defineDocs } from "fumadocs-mdx/config";
+import { pageSchema } from "fumadocs-core/source/schema";
+import { z } from "zod";
+// eyebrow (#8151): the one genuinely per-page datum the old per-route docs..tsx files hard-coded
+// as a JSX prop (e.g. eyebrow="Configuration") instead of deriving it from anything -- moved into
+// frontmatter so the single dynamic docs.$slug.tsx route can read it the same way it reads
+// title/description, with no per-page route file left to hold it. Required (not optional): every real
+// content/docs/*.mdx page has always rendered one, and a page silently missing its eyebrow badge would
+// be a content regression fumadocs-mdx's own build should catch, not something to render as blank.
export const docs = defineDocs({
dir: "content/docs",
+ docs: {
+ schema: pageSchema.extend({
+ eyebrow: z.string(),
+ }),
+ },
});
diff --git a/apps/loopover-ui/src/components/site/ams-observability-callout.tsx b/apps/loopover-ui/src/components/site/ams-observability-callout.tsx
index 00df10396b..67c747cd77 100644
--- a/apps/loopover-ui/src/components/site/ams-observability-callout.tsx
+++ b/apps/loopover-ui/src/components/site/ams-observability-callout.tsx
@@ -12,16 +12,18 @@ import { Callout } from "@/components/site/primitives";
// its own in-app /docs/ams-observability route (#6024, ported from the source packages/loopover-miner/docs/observability.md
// this constant used to point at directly) -- an in-app Link keeps the reader on the docs site instead of bouncing
// to GitHub.
-export const AMS_OBSERVABILITY_DOC_URL = "/docs/ams-observability";
+export const AMS_OBSERVABILITY_DOC_SLUG = "ams-observability";
/** A `note` callout pointing a dual-role ORB+AMS operator at the "Observing your miner" observability guide. */
export function AmsObservabilityCallout() {
return (
If you also run AMS (the loopover-miner) on this host, see{" "}
- Observing your miner to point Grafana at the
- redacted AMS ledger datasources and load its Grafana dashboard — separate from the ORB
- review-service observability above.
+
+ Observing your miner
+ {" "}
+ to point Grafana at the redacted AMS ledger datasources and load its Grafana dashboard —
+ separate from the ORB review-service observability above.
);
}
diff --git a/apps/loopover-ui/src/components/site/api-status-banner.tsx b/apps/loopover-ui/src/components/site/api-status-banner.tsx
index d7f1ef8a65..9088f31783 100644
--- a/apps/loopover-ui/src/components/site/api-status-banner.tsx
+++ b/apps/loopover-ui/src/components/site/api-status-banner.tsx
@@ -106,7 +106,8 @@ export function ApiStatusBanner() {
)}
diff --git a/apps/loopover-ui/src/components/site/app-panels/miner-panel.tsx b/apps/loopover-ui/src/components/site/app-panels/miner-panel.tsx
index b51f4a30d1..f0f044451a 100644
--- a/apps/loopover-ui/src/components/site/app-panels/miner-panel.tsx
+++ b/apps/loopover-ui/src/components/site/app-panels/miner-panel.tsx
@@ -325,7 +325,8 @@ export function MinerPanel() {
Each blocker links to how to clear it.{" "}
See scoreability docs →
diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx
index bab4b2a82a..d5a5f415dc 100644
--- a/apps/loopover-ui/src/components/site/docs-nav.tsx
+++ b/apps/loopover-ui/src/components/site/docs-nav.tsx
@@ -1,4 +1,5 @@
import { Link, useRouterState } from "@tanstack/react-router";
+import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
@@ -116,6 +117,34 @@ function groupItems(group: DocsGroup): DocsItem[] {
return "items" in group ? group.items : group.subgroups.flatMap((sub) => sub.items);
}
+// Every DocsItem#to is either the bare docs index ("/docs") or "/docs/" resolved through the
+// single dynamic docs.$slug.tsx route (#8151) -- items stay full paths (not bare slugs) so pathname
+// comparisons/key/prev-next lookups below don't need a second derived field, and this one helper
+// splits into whichever shape each case needs (the index route takes no params; the dynamic
+// route requires them).
+function DocsNavLink({
+ to,
+ className,
+ children,
+}: {
+ to: string;
+ className: string;
+ children: ReactNode;
+}) {
+ if (to === "/docs") {
+ return (
+
+ {children}
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+}
+
function DocsItemList({ items, pathname }: { items: DocsItem[]; pathname: string }) {
return (
Install on GitHub
Read the onboarding guide
@@ -125,7 +127,8 @@ export function InstallPage() {
choose. You confirm the exact scopes on GitHub before the install completes, and you can
review or revoke them at any time from your repository's installed-Apps settings. See{" "}
privacy & security
diff --git a/apps/loopover-ui/src/routes/install.permissions.test.tsx b/apps/loopover-ui/src/routes/install.permissions.test.tsx
index 03bbfb8918..59d960282d 100644
--- a/apps/loopover-ui/src/routes/install.permissions.test.tsx
+++ b/apps/loopover-ui/src/routes/install.permissions.test.tsx
@@ -2,9 +2,25 @@ import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { ReactNode } from "react";
+// Interpolates $-params the same way the real router resolves `to`/`params` into a concrete href
+// (#8151 moved every docs link from a plain string `to` to the typed `to="/docs/$slug" params={{ slug }}`
+// form), so this stub's rendered link stays a faithful stand-in.
vi.mock("@tanstack/react-router", () => ({
createFileRoute: () => () => ({}),
- Link: ({ to, children }: { to: string; children: ReactNode }) => {children},
+ Link: ({
+ to,
+ params,
+ children,
+ }: {
+ to: string;
+ params?: Record;
+ children: ReactNode;
+ }) => {
+ const href = params
+ ? to.replace(/\$([a-zA-Z0-9_]+)/g, (_match, name: string) => params[name] ?? `$${name}`)
+ : to;
+ return {children};
+ },
}));
import { InstallPermissionsPage } from "./install.permissions";
diff --git a/apps/loopover-ui/src/routes/install.permissions.tsx b/apps/loopover-ui/src/routes/install.permissions.tsx
index e984fde7bf..750d9dd3af 100644
--- a/apps/loopover-ui/src/routes/install.permissions.tsx
+++ b/apps/loopover-ui/src/routes/install.permissions.tsx
@@ -204,7 +204,8 @@ export function InstallPermissionsPage() {
You grant these on only the repositories you choose, and you can review or revoke them
at any time from your repository's installed-Apps settings on GitHub. See{" "}
privacy & security
diff --git a/apps/loopover-ui/src/routes/install.test.tsx b/apps/loopover-ui/src/routes/install.test.tsx
index 187de1a86e..8005de35b5 100644
--- a/apps/loopover-ui/src/routes/install.test.tsx
+++ b/apps/loopover-ui/src/routes/install.test.tsx
@@ -2,9 +2,25 @@ import { render, screen, within } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { ReactNode } from "react";
+// Interpolates $-params the same way the real router resolves `to`/`params` into a concrete href
+// (#8151 moved every docs link from a plain string `to` to the typed `to="/docs/$slug" params={{ slug }}`
+// form), so this stub's rendered link stays a faithful stand-in.
vi.mock("@tanstack/react-router", () => ({
createFileRoute: () => () => ({}),
- Link: ({ to, children }: { to: string; children: ReactNode }) => {children},
+ Link: ({
+ to,
+ params,
+ children,
+ }: {
+ to: string;
+ params?: Record;
+ children: ReactNode;
+ }) => {
+ const href = params
+ ? to.replace(/\$([a-zA-Z0-9_]+)/g, (_match, name: string) => params[name] ?? `$${name}`)
+ : to;
+ return {children};
+ },
}));
import { InstallPage } from "./install.index";
diff --git a/apps/loopover-ui/src/routes/maintainers.tsx b/apps/loopover-ui/src/routes/maintainers.tsx
index bb9be81399..6ee735ec84 100644
--- a/apps/loopover-ui/src/routes/maintainers.tsx
+++ b/apps/loopover-ui/src/routes/maintainers.tsx
@@ -56,13 +56,15 @@ function MaintainersPage() {
Self-host reviews
Maintainer workflow
@@ -230,7 +232,8 @@ GET /v1/repos/:owner/:repo/registration-readiness`}
Open the preview
Self-host reviews
diff --git a/apps/loopover-ui/src/routes/miners.tsx b/apps/loopover-ui/src/routes/miners.tsx
index 02c21b4baf..3c47eae047 100644
--- a/apps/loopover-ui/src/routes/miners.tsx
+++ b/apps/loopover-ui/src/routes/miners.tsx
@@ -55,13 +55,15 @@ function MinersPage() {
Install MCP
Read the workflow
@@ -215,7 +217,8 @@ function MinersPage() {