From 41ec8ab0eee1760cb90403d48a01dea2d94c520d Mon Sep 17 00:00:00 2001
From: oktofeesh1 <287075021+oktofeesh1@users.noreply.github.com>
Date: Thu, 16 Jul 2026 06:09:16 +0200
Subject: [PATCH] docs(ams): port docs/config-precedence.md to a website docs
page (docs.ams-config-precedence.tsx)
Add content/docs/ams-config-precedence.mdx covering AMS's per-concern
configuration layering (per-repo goal spec, operator env, CLI flags,
operator policy file) for kill switch, governor live mode, coding-agent
execution mode, forge credential resolution, GitHub token resolution, and
local store paths -- rendered via the existing DocsPage/Callout/CodeBlock/
FeatureRow primitives. Add the page to docs-nav.tsx's "AMS: deployment"
subgroup and to docs.index.tsx's Maintainers audience card.
packages/loopover-miner/docs/config-precedence.md stays as the canonical
source (ships inside the published @loopover/miner package) with a short
pointer added to the new website page.
---
.../content/docs/ams-config-precedence.mdx | 186 ++++++++++++++++++
.../src/components/site/docs-nav.tsx | 1 +
apps/loopover-ui/src/routeTree.gen.ts | 21 ++
.../src/routes/docs.ams-config-precedence.tsx | 49 +++++
apps/loopover-ui/src/routes/docs.index.tsx | 1 +
.../loopover-miner/docs/config-precedence.md | 4 +
6 files changed, 262 insertions(+)
create mode 100644 apps/loopover-ui/content/docs/ams-config-precedence.mdx
create mode 100644 apps/loopover-ui/src/routes/docs.ams-config-precedence.tsx
diff --git a/apps/loopover-ui/content/docs/ams-config-precedence.mdx b/apps/loopover-ui/content/docs/ams-config-precedence.mdx
new file mode 100644
index 0000000000..b86de459f5
--- /dev/null
+++ b/apps/loopover-ui/content/docs/ams-config-precedence.mdx
@@ -0,0 +1,186 @@
+---
+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.
+---
+
+AMS does **not** have a single `config.js` resolver. Configuration is layered by concern across
+several modules under `packages/loopover-miner/lib/` and `@loopover/engine`. This document states
+the order each layer **actually implements today** — not an idealized or corrected order.
+
+## Configuration layers
+
+ ... argv, scoped to one invocation. Modules: lib/attempt-cli.js, lib/discover-cli.js, lib/loop-cli.js, and others.",
+ },
+ {
+ title: "Operator file (not goal spec)",
+ description:
+ "~/.config/loopover-miner/.loopover-ams.yml, operator execution policy. Module: lib/ams-policy.js.",
+ },
+ ]}
+/>
+
+`.loopover-miner.yml` is **maintainer-authored in the target repo**. Operator env and CLI flags
+are **never overridden by a target repo's goal spec** for operator-owned policy (see
+`lib/ams-policy.js`'s header).
+
+## .loopover-miner.yml file discovery
+
+First existing file wins (engine `MINER_GOAL_SPEC_FILENAMES`):
+
+**1.** `.loopover-miner.yml`
+
+**2.** `.github/loopover-miner.yml`
+
+**3.** `.loopover-miner.json`
+
+**4.** `.github/loopover-miner.json`
+
+## Precedence by concern
+
+### Kill switch (halt miner writes)
+
+**Sources:** `LOOPOVER_MINER_KILL_SWITCH` (operator env) and `.loopover-miner.yml` →
+`killSwitch.paused`.
+
+**Order (safest wins, engine `resolveMinerKillSwitch`):**
+
+**1.** Global env halt → scope `"global"` (always reported even when the repo yml also pauses).
+
+**2.** Else per-repo yml `killSwitch.paused: true` → scope `"repo"`.
+
+**3.** Else → scope `"none"`.
+
+
+ There is **no CLI flag** for kill-switch today. `MINER_CODING_AGENT_PAUSED` is a separate axis
+ (coding-agent spawn only) and does not change kill-switch scope.
+
+
+### Governor live write mode
+
+**Sources:** `LOOPOVER_MINER_LIVE_MODE=live` (operator env) and `.loopover-miner.yml` →
+`execution.liveModeOptIn: live`.
+
+**Order (engine `resolveMinerActionMode`):**
+
+**1.** Kill switch active → `"paused"` (overrides any live opt-in).
+
+**2.** Else **both** operator env **and** repo yml must equal the exact string `"live"` →
+`"live"`.
+
+**3.** Else → `"dry_run"`.
+
+
+ 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).
+
+
+### Coding-agent execution mode (spawn the driver?)
+
+**Sources:** `MINER_CODING_AGENT_PAUSED` (operator env) and `attempt|loop --live` (CLI, per
+invocation).
+
+**Order (engine `resolveCodingAgentExecutionMode`, wired in `lib/attempt-cli.js`):**
+
+**1.** Global env pause (`MINER_CODING_AGENT_PAUSED` truthy) → `"paused"`.
+
+**2.** Else CLI `--live` absent → `agentDryRun: true` → `"dry_run"` (`attempt-cli.js` enforces
+dry-run default).
+
+**3.** Else CLI `--live` present → `"live"`.
+
+
+ There is no `.loopover-miner.yml` field for coding-agent mode today.
+
+
+### Discover forge credential env var name
+
+**Sources:** `discover --token-env ` (CLI), programmatic `options.tokenEnv`, forge default
+(`GITHUB_TOKEN`).
+
+**Order (`lib/discover-cli.js`):**
+
+**1.** CLI `--token-env`
+
+**2.** Else programmatic `options.tokenEnv`
+
+**3.** Else `resolveForgeConfig(...).tokenEnvVar` (default `GITHUB_TOKEN`)
+
+
+ There is no `.loopover-miner.yml` forge block today; `--api-base-url` follows the same CLI →
+ programmatic → default shape for the API host.
+
+
+### GitHub token value
+
+**Sources:** `GITHUB_TOKEN` (operator env), a `loopover-mcp login` session recorded in the
+`loopover-mcp` config file (`~/.config/loopover/config.json` by default), programmatic
+`options.githubToken`.
+
+**Order (`lib/github-token-resolution.js`'s `resolveGitHubToken`, called once at the top of each
+CLI entrypoint — `loop`, `attempt`, `init --verify-token`, `manage poll` — then threaded down
+explicitly to every real GitHub caller):**
+
+**1.** Caller-supplied `options.githubToken` (an explicit override passed programmatically) wins
+outright.
+
+**2.** Else `GITHUB_TOKEN` env — an existing self-host operator's PAT setup keeps working
+unchanged, no filesystem or network access.
+
+**3.** Else a live token fetched from the authenticated `loopover-mcp login` session (`POST
+/v1/auth/github/token`, cached in memory for the process's lifetime; a failed fetch is not
+cached, so a later call retries rather than staying stuck).
+
+**4.** Else `null` — the caller's own existing "no token" failure mode applies (git operations
+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.
+
+
+### Local SQLite store paths
+
+**Sources:** per-store `LOOPOVER_MINER_*_DB` env var, then `LOOPOVER_MINER_CONFIG_DIR`, then XDG
+default (`lib/local-store.js`).
+
+Explicit per-store env **wins** over config dir; config dir **wins** over XDG. No CLI or
+goal-spec override.
+
+## Known gaps / inconsistencies
+
+- **No unified precedence** across yml + env + CLI for a single knob — each concern owns its own
+ resolver.
+- **Live execution** spans two independent gates: coding-agent `--live` (spawn) vs governor
+ env+yml (writes). Both must allow live for a full live open-pr attempt.
+- **Forge tenant overrides** (`--api-base-url`, `--token-env`) are CLI/programmatic only;
+ `.loopover-miner.yml` cannot set them yet.
+- **Operator AMS policy** (`.loopover-ams.yml`) is separate from per-repo goal spec; goal spec
+ never overrides operator policy.
+
+
+ If a future change adds yml or CLI for a setting documented here as env-only, this page and
+ `test/unit/miner-config-precedence.test.ts` both need updating.
+
+
+## See also
+
+- [`packages/loopover-miner/docs/miner-goal-spec.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/miner-goal-spec.md) — goal-spec field reference
+- [`packages/loopover-miner/docs/env-reference.md`](https://github.com/JSONbored/loopover/blob/main/packages/loopover-miner/docs/env-reference.md) — generated operator env list
+- ORB's `.loopover.yml` precedence (`yml > DB > defaults`) in [Self-hosting configuration](/docs/self-hosting-configuration) — analogous documentation style, different runtime
diff --git a/apps/loopover-ui/src/components/site/docs-nav.tsx b/apps/loopover-ui/src/components/site/docs-nav.tsx
index 06433fc79a..3c5683df22 100644
--- a/apps/loopover-ui/src/components/site/docs-nav.tsx
+++ b/apps/loopover-ui/src/components/site/docs-nav.tsx
@@ -80,6 +80,7 @@ export const docsNav: DocsGroup[] = [
{ to: "/docs/ams-observability", label: "Observing your miner" },
{ to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
{ to: "/docs/ams-sizing", label: "Resource sizing" },
+ { to: "/docs/ams-config-precedence", label: "Config precedence" },
],
},
],
diff --git a/apps/loopover-ui/src/routeTree.gen.ts b/apps/loopover-ui/src/routeTree.gen.ts
index d01aa0b25d..bac1be6423 100644
--- a/apps/loopover-ui/src/routeTree.gen.ts
+++ b/apps/loopover-ui/src/routeTree.gen.ts
@@ -62,6 +62,7 @@ import { Route as DocsAmsSizingRouteImport } from './routes/docs.ams-sizing'
import { Route as DocsAmsOperationsRunbookRouteImport } from './routes/docs.ams-operations-runbook'
import { Route as DocsAmsObservabilityRouteImport } from './routes/docs.ams-observability'
import { Route as DocsAmsDeploymentRouteImport } from './routes/docs.ams-deployment'
+import { Route as DocsAmsConfigPrecedenceRouteImport } from './routes/docs.ams-config-precedence'
import { Route as DocsAiSummariesRouteImport } from './routes/docs.ai-summaries'
import { Route as AppWorkbenchRouteImport } from './routes/app.workbench'
import { Route as AppRunsRouteImport } from './routes/app.runs'
@@ -360,6 +361,11 @@ const DocsAmsDeploymentRoute = DocsAmsDeploymentRouteImport.update({
path: '/ams-deployment',
getParentRoute: () => DocsRoute,
} as any)
+const DocsAmsConfigPrecedenceRoute = DocsAmsConfigPrecedenceRouteImport.update({
+ id: '/ams-config-precedence',
+ path: '/ams-config-precedence',
+ getParentRoute: () => DocsRoute,
+} as any)
const DocsAiSummariesRoute = DocsAiSummariesRouteImport.update({
id: '/ai-summaries',
path: '/ai-summaries',
@@ -467,6 +473,7 @@ export interface FileRoutesByFullPath {
'/app/runs': typeof AppRunsRoute
'/app/workbench': typeof AppWorkbenchRoute
'/docs/ai-summaries': typeof DocsAiSummariesRoute
+ '/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
@@ -535,6 +542,7 @@ export interface FileRoutesByTo {
'/app/runs': typeof AppRunsRoute
'/app/workbench': typeof AppWorkbenchRoute
'/docs/ai-summaries': typeof DocsAiSummariesRoute
+ '/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
@@ -607,6 +615,7 @@ export interface FileRoutesById {
'/app/runs': typeof AppRunsRoute
'/app/workbench': typeof AppWorkbenchRoute
'/docs/ai-summaries': typeof DocsAiSummariesRoute
+ '/docs/ams-config-precedence': typeof DocsAmsConfigPrecedenceRoute
'/docs/ams-deployment': typeof DocsAmsDeploymentRoute
'/docs/ams-observability': typeof DocsAmsObservabilityRoute
'/docs/ams-operations-runbook': typeof DocsAmsOperationsRunbookRoute
@@ -680,6 +689,7 @@ export interface FileRouteTypes {
| '/app/runs'
| '/app/workbench'
| '/docs/ai-summaries'
+ | '/docs/ams-config-precedence'
| '/docs/ams-deployment'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
@@ -748,6 +758,7 @@ export interface FileRouteTypes {
| '/app/runs'
| '/app/workbench'
| '/docs/ai-summaries'
+ | '/docs/ams-config-precedence'
| '/docs/ams-deployment'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
@@ -819,6 +830,7 @@ export interface FileRouteTypes {
| '/app/runs'
| '/app/workbench'
| '/docs/ai-summaries'
+ | '/docs/ams-config-precedence'
| '/docs/ams-deployment'
| '/docs/ams-observability'
| '/docs/ams-operations-runbook'
@@ -1252,6 +1264,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DocsAmsDeploymentRouteImport
parentRoute: typeof DocsRoute
}
+ '/docs/ams-config-precedence': {
+ id: '/docs/ams-config-precedence'
+ path: '/ams-config-precedence'
+ fullPath: '/docs/ams-config-precedence'
+ preLoaderRoute: typeof DocsAmsConfigPrecedenceRouteImport
+ parentRoute: typeof DocsRoute
+ }
'/docs/ai-summaries': {
id: '/docs/ai-summaries'
path: '/ai-summaries'
@@ -1417,6 +1436,7 @@ const AppRouteWithChildren = AppRoute._addFileChildren(AppRouteChildren)
interface DocsRouteChildren {
DocsAiSummariesRoute: typeof DocsAiSummariesRoute
+ DocsAmsConfigPrecedenceRoute: typeof DocsAmsConfigPrecedenceRoute
DocsAmsDeploymentRoute: typeof DocsAmsDeploymentRoute
DocsAmsObservabilityRoute: typeof DocsAmsObservabilityRoute
DocsAmsOperationsRunbookRoute: typeof DocsAmsOperationsRunbookRoute
@@ -1462,6 +1482,7 @@ interface DocsRouteChildren {
const DocsRouteChildren: DocsRouteChildren = {
DocsAiSummariesRoute: DocsAiSummariesRoute,
+ DocsAmsConfigPrecedenceRoute: DocsAmsConfigPrecedenceRoute,
DocsAmsDeploymentRoute: DocsAmsDeploymentRoute,
DocsAmsObservabilityRoute: DocsAmsObservabilityRoute,
DocsAmsOperationsRunbookRoute: DocsAmsOperationsRunbookRoute,
diff --git a/apps/loopover-ui/src/routes/docs.ams-config-precedence.tsx b/apps/loopover-ui/src/routes/docs.ams-config-precedence.tsx
new file mode 100644
index 0000000000..73483c2eb9
--- /dev/null
+++ b/apps/loopover-ui/src/routes/docs.ams-config-precedence.tsx
@@ -0,0 +1,49 @@
+import { createFileRoute, notFound } from "@tanstack/react-router";
+import { Suspense } from "react";
+
+import { DocsPage } from "@/components/site/docs-page";
+import { docsClientLoader } from "@/lib/docs-client-loader";
+
+// Rendered from content/docs/ams-config-precedence.mdx via fumadocs-mdx's browser entry
+// (docsClientLoader), through the existing DocsPage/Callout/CodeBlock/FeatureRow
+// primitives -- not fumadocs-ui's bundled components. See docs-source.ts's comment
+// for why the loader below resolves only a plain, serializable path string.
+export const Route = createFileRoute("/docs/ams-config-precedence")({
+ loader: async () => {
+ const { docsSource } = await import("@/lib/docs-source");
+ const page = docsSource.getPage(["ams-config-precedence"]);
+ if (!page) throw notFound();
+ return { path: page.path, title: page.data.title, description: page.data.description };
+ },
+ head: () => ({
+ meta: [
+ { title: "Miner config precedence — LoopOver docs" },
+ {
+ name: "description",
+ content:
+ "How AMS layers configuration across per-repo goal spec, operator env, CLI flags, and operator policy files -- the order each concern actually implements today.",
+ },
+ { property: "og:title", content: "Miner config precedence — LoopOver docs" },
+ {
+ property: "og:description",
+ content:
+ "How AMS layers configuration across per-repo goal spec, operator env, CLI flags, and operator policy files -- the order each concern actually implements today.",
+ },
+ { property: "og:url", content: "/docs/ams-config-precedence" },
+ ],
+ links: [{ rel: "canonical", href: "/docs/ams-config-precedence" }],
+ }),
+ component: AmsConfigPrecedence,
+});
+
+function AmsConfigPrecedence() {
+ const { path, title, description } = Route.useLoaderData();
+ const Content = docsClientLoader.getComponent(path);
+ return (
+
+ Loading…
}>
+
+
+
+ );
+}
diff --git a/apps/loopover-ui/src/routes/docs.index.tsx b/apps/loopover-ui/src/routes/docs.index.tsx
index 1bdecda313..a80c6012db 100644
--- a/apps/loopover-ui/src/routes/docs.index.tsx
+++ b/apps/loopover-ui/src/routes/docs.index.tsx
@@ -78,6 +78,7 @@ const AUDIENCES: Audience[] = [
{ to: "/docs/ams-observability", label: "Observing your miner" },
{ to: "/docs/ams-unattended-scheduling", label: "Unattended scheduling" },
{ to: "/docs/ams-sizing", label: "Resource sizing" },
+ { to: "/docs/ams-config-precedence", label: "Config precedence" },
{ to: "/docs/self-hosting-docs-audit", label: "Self-host docs audit" },
{ to: "/docs/maintainer-install-trust", label: "Install & trust guide" },
{ to: "/docs/github-app", label: "GitHub App configuration" },
diff --git a/packages/loopover-miner/docs/config-precedence.md b/packages/loopover-miner/docs/config-precedence.md
index 8f562e7ae6..3261476432 100644
--- a/packages/loopover-miner/docs/config-precedence.md
+++ b/packages/loopover-miner/docs/config-precedence.md
@@ -1,5 +1,9 @@
# Miner config precedence
+> Also published on the docs website: [Miner config precedence](https://loopover.ai/docs/ams-config-precedence)
+> (same content, rendered with search and the rest of the maintainer docs nav). This file remains
+> the canonical source and ships inside the published `@loopover/miner` package.
+
AMS does **not** have a single `config.js` resolver. Configuration is layered by concern across several modules under `packages/loopover-miner/lib/` and `@loopover/engine`. This document states the order each layer **actually implements today** — not an idealized or corrected order.
## Configuration layers