Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions .loopover.yml.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# ============================================================================
# .loopover.yml — per-repo configuration for gittensory CI & gittensory review
# .loopover.yml — per-repo configuration for loopover CI & loopover review
# ============================================================================
#
# Drop this file at the root of any repo gittensory watches to tune how the
# Drop this file at the root of any repo loopover watches to tune how the
# review engine scores, gates, and comments on its pull requests — as
# config-as-code, versioned alongside the project it governs.
#
Expand Down Expand Up @@ -219,14 +219,14 @@ gate:
lockfileIntegrity: off

# CLA / license-compatibility gate (#2564). Confirms contributor license-agreement consent before a PR
# can auto-merge — the gittensory analog of a "CLA assistant" bot. off | advisory | block. Default: off.
# can auto-merge — the loopover analog of a "CLA assistant" bot. off | advisory | block. Default: off.
# advisory — surfaces a cla_consent_missing finding but never blocks.
# block — also hard-blocks (one-shot close for a contributor) when neither detection method
# below confirms consent.
# Config-as-code only — no DB column or dashboard toggle; this can only be set here.
claMode: off
cla:
# A phrase gittensory looks for in the PR description (case-insensitive substring match), mirroring
# A phrase loopover looks for in the PR description (case-insensitive substring match), mirroring
# review.pre_merge_checks' descriptionContains. String or null. Default: null (not configured).
consentPhrase: "I have read and agree to the CLA"
# Name of a separate CLA-bot check-run this repo also runs (e.g. a CLA Assistant GitHub Action). A
Expand Down Expand Up @@ -722,7 +722,7 @@ settings:
# from the PR title; priority (or any other category you register) ONLY from
# `linkedIssueLabelPropagation` above — never inferred from title, files, AI, or existing PR
# labels (#priority-linked-issue-gate).
# 3. Autonomy-outcome labels (`gittensory:ready-to-merge` etc.) — gated by the `review_state_label`
# 3. Autonomy-outcome labels (`loopover:ready-to-merge` etc.) — gated by the `review_state_label`
# autonomy class below, advisory commentary on the bot's own verdict.
# 4. Anti-abuse enforcement labels (blacklist/contributor-cap/review-nag) — gated by the `close`
# autonomy class below, applied alongside the close action they accompany.
Expand All @@ -736,8 +736,8 @@ settings:
# labels (blacklist/contributor-cap/review-nag) ride on the SAME dial as their accompanying close
# (`close`, below) — set `close: auto` and they close-and-label together with no `label` grant needed.
# - `review_state_label` gates the bot's own disposition-communication labels only:
# gittensory:ready-to-merge / gittensory:changes-requested / gittensory:needs-human-review /
# gittensory:migration-collision. These are advisory signals about the bot's own verdict, not
# loopover:ready-to-merge / loopover:changes-requested / loopover:needs-human-review /
# loopover:migration-collision. These are advisory signals about the bot's own verdict, not
# enforcement — for a one-shot review model (merge/close/hold through the required gate check, no
# back-and-forth) leave this at the default `observe` so they never appear; set it to `auto` only if
# you specifically want that running commentary as GitHub labels.
Expand Down Expand Up @@ -824,7 +824,7 @@ settings:
# List of GitHub logins. Default: [] (no logins watched).
# reviewNagMonitoredMentions: [your-maintainer-login]

# Shared repo-scoped exemption list (#2463): GitHub logins never throttled/closed by gittensory's
# Shared repo-scoped exemption list (#2463): GitHub logins never throttled/closed by loopover's
# deterministic anti-abuse mechanisms (review-nag cooldown today; the per-contributor open-item cap
# above will reuse this list too), on top of the standing owner/admin/automation-bot exemption.
# List of GitHub logins. Default: [] (no additional exemptions).
Expand Down Expand Up @@ -856,8 +856,8 @@ settings:
# moderationBannedLabel: mod:banned # Label applied at >= the ban threshold. Default: the global config's bannedLabel.

# Review-evasion protection (#review-evasion-protection, anti-abuse): a contributor closing or converting
# their own PR to draft while gittensory has an ACTIVE review pass running against it is dodging the
# one-shot review process, not making an ordinary close. When enabled, gittensory reopens (if needed) and
# their own PR to draft while loopover has an ACTIVE review pass running against it is dodging the
# one-shot review process, not making an ordinary close. When enabled, loopover reopens (if needed) and
# re-closes the PR as the App -- a close the contributor cannot themselves reopen (#one-shot-reopen) --
# posts an explanation comment, applies the configured label, and records a `review_evasion` moderation
# strike (subject to moderationRules above including it). CLOSE BY DEFAULT as of #4011 -- "off" is an
Expand Down Expand Up @@ -1057,7 +1057,7 @@ settings:
# url_template: "https://pr-{number}.myapp.workers.dev"
# routes:
# # An explicit, always-screenshotted route list. When non-empty, REPLACES automatic file-to-route
# # inference entirely -- for a repo whose routing convention isn't gittensory-ui's TanStack file-based
# # inference entirely -- for a repo whose routing convention isn't loopover-ui's TanStack file-based
# # one. Empty/default ⇒ automatic inference (falling back to "/" when nothing matches).
# paths:
# - "/pricing"
Expand Down Expand Up @@ -1095,11 +1095,11 @@ settings:
# # .github/workflows/visual-capture-fallback.yml -- a fork-safe GitHub Actions job (contents: read, no
# # secrets) that builds, serves, and screenshots the PR's own code, and use its captured PNGs as the
# # "after" shot instead. Requires that workflow file to be present in this repo (copy it from
# # JSONbored/gittensory unmodified -- see the workflow's own header for setup). Bool. Default: false (no
# # dispatch, byte-identical to today). Not needed for gittensory-ui / metagraphed, which already have
# # JSONbored/loopover unmodified -- see the workflow's own header for setup). Bool. Default: false (no
# # dispatch, byte-identical to today). Not needed for loopover-ui / metagraphed, which already have
# # their own preview-deploy pipeline. (#4112, part of the #3607 visual-capture convergence epic)
# actions_fallback: false
# # Maintainer overrides for the public review-panel CONTENT (not what gittensory measures). The
# # Maintainer overrides for the public review-panel CONTENT (not what loopover measures). The
# # Gittensor attribution + register link is always appended to the footer regardless; maintainer text
# # failing the public-safe filter is dropped, never published.
# footer:
Expand Down Expand Up @@ -1156,20 +1156,20 @@ settings:
# screenshots: false
# improvementSignal: false

# Optional ecosystem/network PLUGINS -- distinct from `features:` above, which only toggles gittensory's own
# Optional ecosystem/network PLUGINS -- distinct from `features:` above, which only toggles loopover's own
# converged review capabilities. Each key here couples this instance to an external system and is OFF unless
# BOTH a deployment-wide LOOPOVER_EXPERIMENTAL_* env kill-switch AND an explicit per-repo `true` are set (no
# LOOPOVER_REVIEW_REPOS allowlist fallback -- unlike `features:`, there is no default-on path). `gittensor`
# is the first plugin: gittensory's original subnet mining-registry/scoring integration (per-repo emission
# is the first plugin: loopover's original subnet mining-registry/scoring integration (per-repo emission
# share, maintainer cut, label multipliers pulled from the gittensor subnet's registry), now opt-in rather
# than a core dependency -- a self-host instance that never sets this has zero footprint from it: no fetch
# from the gittensor subnet registry, no local tracking/backfill of other repos on that subnet. Future
# plugins land in this same block as gittensory broadens beyond gittensor.
# plugins land in this same block as loopover broadens beyond gittensor.
# experimental:
# gittensor: true

# Registry-review lane (#2435): lets a self-hosted maintainer point gittensory at their OWN structured
# registry (e.g. a subnet/plugin/package catalog) without a gittensory code change -- reviewing additions
# Registry-review lane (#2435): lets a self-hosted maintainer point loopover at their OWN structured
# registry (e.g. a subnet/plugin/package catalog) without a loopover code change -- reviewing additions
# to a data file the same way it reviews code. Uncomment and set at least entryFileGlob + collectionField
# (both required; the block is ignored with a warning if either is missing).
# contentLane:
Expand Down Expand Up @@ -1204,7 +1204,7 @@ settings:

# Cross-repo maintainer recap digest (#1963, #2250): config-as-code override for the CRON-scheduled digest
# that folds gate-precision + outcome-calibration across every scanned repo into one report (distinct from
# the single-repo reviewRecap above). Operator-level, not per-repo -- only meaningful on the gittensory
# the single-repo reviewRecap above). Operator-level, not per-repo -- only meaningful on the loopover
# self-repo's own manifest (the repo this instance identifies as); a present block there wins over the
# LOOPOVER_MAINTAINER_RECAP / LOOPOVER_RECAP_CADENCE env vars, which stay the fallback when absent.
# maintainerRecap:
Expand Down
3 changes: 0 additions & 3 deletions apps/loopover-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9399,9 +9399,6 @@
"agentDryRun": {
"type": "boolean"
},
"agentGlobalFreezeOverride": {
"type": "boolean"
},
"contributorOpenPrCap": {
"type": "integer",
"nullable": true,
Expand Down
11 changes: 11 additions & 0 deletions migrations/0150_drop_agent_global_freeze_override.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
-- Dead-column cleanup: removes the per-repo escape hatch from the global DB kill-switch (#4372/#4391).
-- global_agent_controls.frozen (isGlobalAgentFrozen) is now an absolute fleet-wide brake with no per-repo
-- bypass, same tier as the AGENT_ACTIONS_PAUSED env var -- day-to-day repo enable/disable is
-- repository_settings.agent_paused instead, which is already resolved through the normal global-default +
-- per-repo-override .loopover.yml config layering (settings.agentPaused). This column let one repo opt out of
-- a fleet-wide freeze while every other repo stayed frozen, but that required either a raw DB write or a
-- container-private config edit gated to source: "api_record" -- an operator-only lever with no real config-
-- as-code parity with every other repository_settings field, and the exact kind of DB-controlled behavior
-- this project's config-as-code convention rules out. SQLite 3.35+ / D1 supports DROP COLUMN directly (same
-- precedent as 0122_drop_private_trust_enabled.sql / 0146_drop_gate_check_mode.sql).
ALTER TABLE repository_settings DROP COLUMN agent_global_freeze_override;
28 changes: 2 additions & 26 deletions packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,6 @@ export type FocusManifestSettings = Partial<
| "autoMaintain"
| "agentPaused"
| "agentDryRun"
| "agentGlobalFreezeOverride"
| "commandAuthorization"
| "contributorBlacklist"
| "blacklistLabel"
Expand Down Expand Up @@ -1798,7 +1797,7 @@ const MAX_REVIEW_NAG_COOLDOWN_DAYS = 365;
* Parse the optional `settings:` mapping — a partial repository-settings override. Only recognized
* fields are kept; unknown/invalid values are dropped with a warning and never throw.
*/
function parseSettingsOverride(value: JsonValue | undefined, warnings: string[], source?: FocusManifestSource): FocusManifestSettings {
function parseSettingsOverride(value: JsonValue | undefined, warnings: string[]): FocusManifestSettings {
if (value === undefined || value === null) return {};
if (typeof value !== "object" || Array.isArray(value)) {
warnings.push(`Manifest field "settings" must be a mapping; ignoring it.`);
Expand Down Expand Up @@ -1862,29 +1861,6 @@ function parseSettingsOverride(value: JsonValue | undefined, warnings: string[],
const flag = normalizeOptionalBoolean(r[key], `settings.${key}`, warnings);
if (flag !== null) out[key] = flag;
}
// agentGlobalFreezeOverride is deliberately NOT in the generic boolean loop above (#4372/#4391/operator-only-
// freeze-fix): it is an OPERATOR-ONLY emergency lever ("re-activate this one repo while the fleet-wide kill-
// switch stays on elsewhere"), and every OTHER settings field in that loop is readable from BOTH the public,
// maintainer-owned `.loopover.yml` committed in the repo's own git history (source: "repo_file") AND the
// operator's private, container-local self-host config (source: "api_record") -- see loadRepoFocusManifestWithCachePolicy
// in focus-manifest-loader.ts for how each source is produced. A repo MAINTAINER must never be able to grant
// their own repo an exemption from the operator's fleet-wide freeze via their own committed yml (that is
// exactly the "scope leak" #4391 closed by stripping this field from the shared loop entirely). But the
// OPERATOR's own private config source is a fundamentally different trust boundary -- it is edited only by
// whoever has filesystem access to the container's private config directory, not by any repo's maintainers --
// and #4391 over-corrected by also removing the operator's own legitimate, config-as-code path for this lever,
// forcing raw undocumented DB writes as the only remaining mechanism (violating this project's config-as-code
// convention: every operator-facing control belongs in the global-default + per-repo-override config files,
// env vars are for bootstrap only). Restore it, gated STRICTLY to the private source.
if (source === "api_record") {
const agentGlobalFreezeOverride = normalizeOptionalBoolean(r.agentGlobalFreezeOverride, "settings.agentGlobalFreezeOverride", warnings);
if (agentGlobalFreezeOverride !== null) out.agentGlobalFreezeOverride = agentGlobalFreezeOverride;
} else if (r.agentGlobalFreezeOverride !== undefined) {
// A public/maintainer-owned manifest attempting to set this is silently dropped, not surfaced as a normal
// "invalid value" warning -- warnings are public-safe text that can reach a contributor-facing preview, and
// this should not teach a non-operator that the field exists or that they almost bypassed the fleet freeze.
warnings.push("Ignored settings.agentGlobalFreezeOverride: operator-only, not settable from a repo-owned manifest.");
}
// Agent-layer autonomy dial (#773): `settings.autonomy` maps each action class to a level. Only set it
// when at least one valid class→level pair survives normalization, so a malformed block never blanks the
// DB-configured policy via the resolver's `{...dbSettings, ...manifest.settings}` overlay.
Expand Down Expand Up @@ -3102,7 +3078,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
maintainerNotes: normalizeStringList(record.maintainerNotes, "maintainerNotes", warnings),
publicNotes: normalizeStringList(record.publicNotes, "publicNotes", warnings).filter(isFocusManifestPublicSafe),
gate: parseGateConfig(record.gate, warnings),
settings: parseSettingsOverride(record.settings, warnings, resolvedSource),
settings: parseSettingsOverride(record.settings, warnings),
review: parseReviewConfig(record.review, warnings),
features: parseFeaturesConfig(record.features, warnings),
experimental: parseExperimentalConfig(record.experimental, warnings),
Expand Down
5 changes: 0 additions & 5 deletions packages/loopover-engine/src/types/manifest-deps-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,11 +527,6 @@ export type RepositorySettings = {
/** Per-repo dry-run/shadow mode (#776): when true, the action layer records what it WOULD do without
* performing any GitHub mutation. Default false. */
agentDryRun?: boolean | undefined;
/** Per-repo override of the global DB-backed agent freeze (#4372): when true, this repo's actions execute
* even while `global_agent_controls.frozen` is set, so an operator can re-activate one repo at a time
* without lifting the fleet-wide brake. Never overrides the `AGENT_ACTIONS_PAUSED` env var, and
* {@link agentPaused} on this same repo still wins over it. Default false. */
agentGlobalFreezeOverride?: boolean | undefined;
/** Moderation-rules engine (#selfhost-mod-engine): whether the whole layer runs on THIS repo. `"inherit"`
* (the DB default) defers to `global_moderation_config.enabled`; `"off"`/`"enabled"` force this repo
* regardless of the global default. Always populated by the DB layer; optional so existing settings
Expand Down
16 changes: 3 additions & 13 deletions scripts/check-docs-drift.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
// against .loopover.yml.example. Nothing else in CI catches a docs page/example silently falling behind when
// a new flag/command/gate-mode/settings/manifest field is added to source but the place documenting that
// surface is never updated -- a reviewer has to notice by eye, and often doesn't (#4617's own audit found
// `agentGlobalFreezeOverride` and `review.visual.production_url` this way: both fully live in code, neither
// mentioned anywhere a maintainer would think to look).
// `review.visual.production_url` this way: fully live in code, but not mentioned anywhere a maintainer would
// think to look).
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -90,20 +90,11 @@ export function extractRepositorySettingsFields(typesText) {
}

/** RepositorySettings fields deliberately excluded from the "every field must have SOME
* `.loopover.yml.example` mention" check below, for three distinct reasons -- flagging any as "undocumented"
* `.loopover.yml.example` mention" check below, for two distinct reasons -- flagging any as "undocumented"
* would be a false drift signal, not a real gap:
* - Not a maintainer-settable knob at all: `repoFullName` is the row's own identity key (set once at
* creation, the opposite of something a maintainer overrides via config); `createdAt`/`updatedAt` are
* DB-row bookkeeping timestamps.
* - `agentGlobalFreezeOverride`: genuinely settable, but DELIBERATELY never documented in the PUBLIC
* `.loopover.yml.example` -- it is settable only from the self-host operator's own PRIVATE config
* (`source: "api_record"` in `parseSettingsOverride`, packages/loopover-engine/src/focus-manifest.ts),
* never from a repo's own committed, maintainer-owned manifest (#4391's scope-leak fix). Documenting it in
* the public example would misleadingly suggest a repo maintainer can set it themselves -- see the same
* exclusion, with the same rationale, in `SETTINGS_OPERATOR_ONLY_FIELDS` in
* test/unit/focus-manifest.test.ts's `.loopover.yml.example field-exhaustiveness` suite. (An #4617 audit
* pass first flagged this field as an undocumented gap without that context; cross-checking the existing
* exhaustiveness suite before "fixing" it here caught the false positive.)
* - `skipAutomationBotAuthors`: genuinely settable (global env default + per-repo `inherit`/`off`/`enabled`
* override, mirroring `moderationGateMode`'s shape), but DELIBERATELY not wired into the
* FocusManifest/`.loopover.yml` parsing path -- DB-only for now, confirmed as an intentional scope choice
Expand All @@ -113,7 +104,6 @@ const NOT_YML_CONFIGURABLE_SETTINGS_FIELDS = new Set([
"repoFullName",
"createdAt",
"updatedAt",
"agentGlobalFreezeOverride",
"skipAutomationBotAuthors",
]);

Expand Down
Loading
Loading