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
11 changes: 10 additions & 1 deletion .loopover.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -1154,7 +1154,7 @@ settings:

# Per-repo activation overrides for the converged review features that ship behind a deployment-wide
# LOOPOVER_REVIEW_* env kill-switch (rag/reputation/safety/grounding/e2eTests/screenshots/
# improvementSignal). Each key is `true` (force on for this repo, subject to the env flag still being
# improvementSignal/amsReputationBridge). Each key is `true` (force on for this repo, subject to the env flag still being
# enabled), `false` (force off), or omitted (falls back to the LOOPOVER_REVIEW_REPOS allowlist default --
# an operator who sets nothing keeps today's behavior). Exception: `safety` is force-on-only -- an untrusted
# repo-controlled `false` is treated as "no opinion" rather than an active force-off (#2269), since a
Expand All @@ -1171,6 +1171,14 @@ settings:
# security-hardening or full-file-fetch rationale that would justify a force-on/force-off asymmetry.
# `improvementSignal` is activation wiring only for now -- no tier reads the resolved
# value yet.
# `amsReputationBridge` (#6485, per #6208's decided design) is a plain symmetric override too. It lets a
# submitter's genuine track record on a LOCAL AMS instance UPGRADE their reputation standing here -- toward
# `trusted` only, never toward `low`, so an AMS record can never be used punitively against a contributor.
# ORB pulls from AMS (never AMS pushing in), so an arbitrary AMS instance cannot write trust signals into
# this one. Needs BOTH the LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE env kill-switch AND
# LOOPOVER_AMS_TRACK_RECORD_URL (the AMS base URL to pull from) -- with either unset the bridge applies no
# bonus signal and the reputation path is byte-identical to today. An unreachable/slow/absent AMS degrades
# the same way: no bonus, never a throw into the gate.
# features:
# rag: true
# reputation: false
Expand All @@ -1179,6 +1187,7 @@ settings:
# e2eTests: false
# screenshots: false
# improvementSignal: false
# amsReputationBridge: false

# 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
Expand Down
1 change: 1 addition & 0 deletions apps/loopover-ui/content/docs/privacy-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ LOOPOVER_REVIEW_PLANNER="true" # @loopover plan on-demand implem
LOOPOVER_REVIEW_SCREENSHOTS="true" # before/after visual capture for UI changes
LOOPOVER_REVIEW_E2E_TESTS="true" # AI-generated E2E test coverage (needs features.e2eTests too)
LOOPOVER_REVIEW_IMPROVEMENT_SIGNAL="true" # read-only PR quality-delta signal (activation only, no-op for now)
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)
Expand Down
7 changes: 7 additions & 0 deletions apps/loopover-ui/content/docs/tuning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ any per-PR feature to run on a given repo. So a per-PR feature activates only wh
Off by default; config-as-code activation only for now — no tier reads the resolved value
yet, so turning this on has no visible effect until a later release wires real behavior
behind it. Per-PR.
- `LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE` — master kill-switch for the upgrade-only
ORB/AMS reputation bridge: a submitter's genuine track record on a local AMS instance can
raise their reputation standing here, never lower it. Off by default; a repo also needs its
own `features.amsReputationBridge: true` override in `.loopover.yml`, and the operator must
point `LOOPOVER_AMS_TRACK_RECORD_URL` at the AMS instance to pull from — with either unset
the bridge applies no bonus signal. ORB pulls from AMS (never the reverse), and an
absent/unreachable/slow AMS simply yields no bonus. Per-PR.
- `LOOPOVER_REVIEW_CONTINUOUS` — fleet-wide default AI review re-trigger cadence.
Off by default (one-shot): AI-generated content (main review, slop advisory, linked-issue
satisfaction) is produced once per PR and never regenerated automatically afterward — only
Expand Down
11 changes: 10 additions & 1 deletion config/examples/loopover.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1168,7 +1168,7 @@ settings:

# Per-repo activation overrides for the converged review features that ship behind a deployment-wide
# LOOPOVER_REVIEW_* env kill-switch (rag/reputation/safety/grounding/e2eTests/screenshots/
# improvementSignal). Each key is `true` (force on for this repo, subject to the env flag still being
# improvementSignal/amsReputationBridge). Each key is `true` (force on for this repo, subject to the env flag still being
# enabled), `false` (force off), or omitted (falls back to the LOOPOVER_REVIEW_REPOS allowlist default --
# an operator who sets nothing keeps today's behavior). Exception: `safety` is force-on-only -- an untrusted
# repo-controlled `false` is treated as "no opinion" rather than an active force-off (#2269), since a
Expand All @@ -1185,6 +1185,14 @@ settings:
# security-hardening or full-file-fetch rationale that would justify a force-on/force-off asymmetry.
# `improvementSignal` is activation wiring only for now -- no tier reads the resolved
# value yet.
# `amsReputationBridge` (#6485, per #6208's decided design) is a plain symmetric override too. It lets a
# submitter's genuine track record on a LOCAL AMS instance UPGRADE their reputation standing here -- toward
# `trusted` only, never toward `low`, so an AMS record can never be used punitively against a contributor.
# ORB pulls from AMS (never AMS pushing in), so an arbitrary AMS instance cannot write trust signals into
# this one. Needs BOTH the LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE env kill-switch AND
# LOOPOVER_AMS_TRACK_RECORD_URL (the AMS base URL to pull from) -- with either unset the bridge applies no
# bonus signal and the reputation path is byte-identical to today. An unreachable/slow/absent AMS degrades
# the same way: no bonus, never a throw into the gate.
# features:
# rag: true
# reputation: false
Expand All @@ -1193,6 +1201,7 @@ settings:
# e2eTests: false
# screenshots: false
# improvementSignal: false
# amsReputationBridge: false

# 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
Expand Down
12 changes: 11 additions & 1 deletion packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,16 @@ export type CopycatGateMode = "off" | "warn" | "label" | "block";
// level `false` behaves like any other plain override with no floor/ceiling. This is activation wiring only
// -- no tier reads the resolved value yet (sibling sub-issues #4739-#4746 build the deterministic/LLM/panel
// behavior that will gate on it).
export const CONVERGED_FEATURE_KEYS = ["rag", "reputation", "safety", "grounding", "e2eTests", "screenshots", "improvementSignal"] as const;
export const CONVERGED_FEATURE_KEYS = [
"rag",
"reputation",
"safety",
"grounding",
"e2eTests",
"screenshots",
"improvementSignal",
"amsReputationBridge",
] as const;
export type ConvergedFeatureKey = (typeof CONVERGED_FEATURE_KEYS)[number];

/** Per-repo activation overrides for the converged review features (`features:` block). `true`/`false` force the
Expand Down Expand Up @@ -1141,6 +1150,7 @@ const EMPTY_FEATURES_CONFIG: FocusManifestFeaturesConfig = {
e2eTests: null,
screenshots: null,
improvementSignal: null,
amsReputationBridge: null,
};

const EMPTY_EXPERIMENTAL_CONFIG: FocusManifestExperimentalConfig = {
Expand Down
11 changes: 11 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,17 @@ declare global {
* build the deterministic/LLM/panel behavior that will gate on it. Default OFF — unset/false the feature
* is never active for any repo regardless of a per-repo `features.improvementSignal` override. */
LOOPOVER_REVIEW_IMPROVEMENT_SIGNAL?: string;
/** #6485 (per #6208's decided design): master kill-switch for the `amsReputationBridge` converged feature —
* an UPGRADE-ONLY, pull-based bridge letting a submitter's genuine AMS track record improve (never worsen)
* their ORB reputation standing. Default OFF — unset/false the bridge is never active for any repo
* regardless of a per-repo `features.amsReputationBridge` override, so the reputation path is
* byte-identical to today. Needs LOOPOVER_AMS_TRACK_RECORD_URL to have anything to pull from. */
LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE?: string;
/** #6485: the operator-configured base URL of the LOCAL AMS instance the reputation bridge pulls a
* submitter's track record from (`GET {url}/track-record/{login}`). Unset ⇒ the bridge applies no bonus
* signal even when enabled. ORB pulls (never AMS pushing in), so an arbitrary AMS instance can never write
* trust signals into ORB — see #6208. */
LOOPOVER_AMS_TRACK_RECORD_URL?: string;
/** #one-shot-review-cadence: the operator's FLEET-WIDE default for AI review re-trigger cadence, consulted
* only when a repo's `.loopover.yml review.auto_review.cadence` is unset (a per-repo value always wins
* regardless of this flag — see resolveAiReviewCadence). Default OFF (unset/false) ⇒ "one_shot": the
Expand Down
29 changes: 29 additions & 0 deletions src/review/ams-reputation-bridge-wire.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Convergence (ORB/AMS reputation bridge, #6485, per #6208's decided design): the master kill-switch for the
// `amsReputationBridge` converged feature -- an UPGRADE-ONLY, pull-based bridge that lets a submitter's genuine
// AMS track record improve (never worsen) their ORB reputation standing. Mirrors the shape of
// `improvement-signal-wire.ts`/`rag-wire.ts`: this file is deliberately just the env flag, so the
// per-repo `features.amsReputationBridge` override in `.loopover.yml` has a global switch to gate on.
//
// Single env switch: LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE. Default OFF (unset/"false") -- when OFF the bridge
// never runs for any repo, regardless of a per-repo `.loopover.yml` override (see `resolveConvergedFeature` in
// `./feature-activation`), so the reputation path is byte-identical to today. Truthy follows the codebase
// convention (`/^(1|true|yes|on)$/i`, same as isReputationEnabled / isImprovementSignalEnabled).
//
// STRICTLY INTERNAL: like the `reputation` signal it extends, the bridged standing NEVER appears in any public
// comment, label, or check-run -- it only routes the private, server-side AI-spend decision.

/** True when the ORB/AMS reputation bridge is enabled at the deployment level. Flag-OFF (default) → the bridge
* is never active for any repo, regardless of a per-repo `features.amsReputationBridge` override. */
export function isAmsReputationBridgeEnabled(env: {
LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE?: string | undefined;
}): boolean {
return /^(1|true|yes|on)$/i.test((env.LOOPOVER_REVIEW_AMS_REPUTATION_BRIDGE ?? "").trim());
}

/** The operator-configured LOCAL AMS base URL the bridge pulls from, or undefined when unset/blank (⇒ the
* bridge applies no bonus signal even when the feature is otherwise active). Kept next to the kill-switch so
* both halves of this feature's deployment config resolve in one place. */
export function resolveAmsTrackRecordEndpoint(env: { LOOPOVER_AMS_TRACK_RECORD_URL?: string | undefined }): string | undefined {
const url = (env.LOOPOVER_AMS_TRACK_RECORD_URL ?? "").trim();
return url === "" ? undefined : url;
}
123 changes: 123 additions & 0 deletions src/review/ams-reputation-bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// ORB/AMS reputation bridge (#6485), implementing #6208's decided design. ORB PULLS a submitter's AMS track
// record on demand and may only ever UPGRADE the locally-computed ReputationSignal toward "trusted" -- never
// downgrade it. Rationale (from #6208): a push model would let any AMS instance -- including a self-hosted one
// running against an arbitrary repo -- write arbitrary trust signals into ORB's internal reputation store, a
// direct gaming vector. Pull keeps ORB in control; upgrade-only closes the second vector (an AMS track record
// must never be usable punitively against a contributor's standing on an unrelated repo).
//
// Identity is plain `authorLogin` -- the same axis `submitter-reputation.ts` already keys on, and the same axis
// `TrackRecordPullRequestOutcome` already carries. No new identity system, and deliberately NOT hotkey/wallet
// (forbidden/redacted terms in this codebase).
//
// Privacy: `TrackRecordPullRequestOutcome` has no score/ranking/wallet/hotkey fields by construction, so the
// consumed shape is already safe -- nothing new to redact. STRICTLY INTERNAL, inherited from
// `submitter-reputation.ts`: never surfaced in a label, comment, or check-run.
//
// Fail-safe contract, matching every other guard in the reputation path: any fetch error, timeout, non-OK
// status, or malformed payload degrades to "no bonus signal applied" -- this must never throw into the gate.

import type { TrackRecordPullRequestOutcome } from "@loopover/engine";
import type { ReputationSignal } from "./submitter-reputation";

/** A slow or unreachable AMS instance must never slow gate evaluation -- a few hundred ms, consistent with the
* other fail-safe external reads in this codebase. */
export const AMS_TRACK_RECORD_TIMEOUT_MS = 400;

/** `trusted` needs at least this many merged AMS PRs. Mirrors `submitter-reputation.ts`'s "default GENEROUS"
* philosophy: a sparse record is simply no bonus, never a penalty. */
export const AMS_BRIDGE_TRUSTED_MIN_MERGED = 3;

/** …AND a merge rate at/above this share of that submitter's terminal AMS outcomes (0–1). */
export const AMS_BRIDGE_TRUSTED_MIN_MERGE_RATE = 0.6;

export type AmsTrackRecordFetch = (url: string, init: RequestInit) => Promise<Response>;

export type AmsBridgeOptions = {
/** The operator-configured AMS endpoint base (`LOOPOVER_AMS_TRACK_RECORD_URL`). */
endpoint: string | undefined;
fetchImpl?: AmsTrackRecordFetch | undefined;
timeoutMs?: number | undefined;
};

/** PURE: is a raw value a usable TrackRecordPullRequestOutcome for this bridge? Only the two fields the bridge
* actually reads are required -- a payload carrying extra keys is fine (and any score-ish key is simply never
* read), but one missing `authorLogin`/`state` is malformed and dropped. */
function isUsableOutcome(value: unknown): value is TrackRecordPullRequestOutcome {
if (value === null || typeof value !== "object") return false;
const row = value as Record<string, unknown>;
return typeof row.authorLogin === "string" && row.authorLogin.trim() !== "" && typeof row.state === "string";
}

/** PURE: keep only this login's outcomes from a payload. Case-insensitive, matching GitHub login semantics. */
export function outcomesForLogin(rows: readonly unknown[], login: string): TrackRecordPullRequestOutcome[] {
const wanted = login.trim().toLowerCase();
if (!wanted) return [];
return rows.filter(isUsableOutcome).filter((row) => row.authorLogin.trim().toLowerCase() === wanted);
}

/**
* PURE: does this AMS track record qualify the submitter as `trusted`? Deliberately a single boolean rather
* than a full ReputationSignal: the bridge is upgrade-only, so "does not qualify" and "looks bad" are the same
* outcome (no bonus) -- there is no representable way for AMS data to push a submitter down.
*/
export function amsRecordQualifiesAsTrusted(outcomes: readonly TrackRecordPullRequestOutcome[]): boolean {
const merged = outcomes.filter((o) => o.state === "merged").length;
// Only TERMINAL outcomes form the denominator: an open PR is not yet evidence either way.
const terminal = outcomes.filter((o) => o.state === "merged" || o.state === "closed").length;
if (merged < AMS_BRIDGE_TRUSTED_MIN_MERGED) return false;
/* v8 ignore next -- merged >= 3 above guarantees terminal >= 3, so terminal is never 0 here; the guard is kept
so a future caller passing pre-filtered rows can never divide by zero. */
if (terminal === 0) return false;
return merged / terminal >= AMS_BRIDGE_TRUSTED_MIN_MERGE_RATE;
}

/**
* PURE: the upgrade-only merge. `neutral`/`low` may move to `trusted` when AMS vouches; nothing else changes.
* An already-`trusted` submitter is unaffected, and no input can ever move a submitter DOWN -- the whole point
* of #6208's upgrade-only weighting.
*/
export function upgradeReputationSignal(local: ReputationSignal, amsTrusted: boolean): ReputationSignal {
return amsTrusted ? "trusted" : local;
}

/**
* Fetch a login's AMS track record from the operator-configured endpoint. Returns null -- meaning "no bonus
* signal applied" -- when the bridge has no endpoint configured, the login is blank, or the call fails in ANY
* way (network error, timeout, non-OK status, non-array/malformed body). Never throws.
*/
export async function fetchAmsTrackRecord(login: string, options: AmsBridgeOptions): Promise<TrackRecordPullRequestOutcome[] | null> {
const endpoint = typeof options.endpoint === "string" ? options.endpoint.trim().replace(/\/+$/, "") : "";
const submitter = typeof login === "string" ? login.trim() : "";
if (!endpoint || !submitter) return null;
const fetchImpl = options.fetchImpl ?? fetch;
const timeoutMs = options.timeoutMs ?? AMS_TRACK_RECORD_TIMEOUT_MS;
try {
const response = await fetchImpl(`${endpoint}/track-record/${encodeURIComponent(submitter)}`, {
method: "GET",
headers: { accept: "application/json" },
signal: AbortSignal.timeout(timeoutMs),
});
if (!response.ok) return null;
const payload: unknown = await response.json();
// Accept either a bare array or a { pullRequests: [...] } envelope; anything else is malformed → no bonus.
const rows = Array.isArray(payload) ? payload : Array.isArray((payload as { pullRequests?: unknown })?.pullRequests) ? (payload as { pullRequests: unknown[] }).pullRequests : null;
if (!rows) return null;
return outcomesForLogin(rows, submitter);
} catch {
return null; // fail-safe: unreachable / timed out / malformed JSON ⇒ no bonus, never a throw into the gate.
}
}

/**
* The bridge entry point: given the locally-computed signal, return the possibly-UPGRADED signal for `login`.
* Callers must only invoke this once `resolveConvergedFeature(env, manifest, "amsReputationBridge", repo)` is
* true; when the feature is off this is never reached and the local signal stands, byte-identical to today.
*/
export async function bridgeAmsReputation(local: ReputationSignal, login: string | undefined, options: AmsBridgeOptions): Promise<ReputationSignal> {
// Already at the ceiling — skip the network entirely; an upgrade-only bridge has nothing to add.
if (local === "trusted") return local;
if (!login) return local;
const outcomes = await fetchAmsTrackRecord(login, options);
if (outcomes === null) return local;
return upgradeReputationSignal(local, amsRecordQualifiesAsTrusted(outcomes));
}
2 changes: 2 additions & 0 deletions src/review/feature-activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { isConvergenceRepoAllowed } from "./cutover-gate";
import { isE2eTestGenerationEnabled } from "./e2e-test-gen-wire";
import { isGroundingEnabled } from "./grounding-wire";
import { isImprovementSignalEnabled } from "./improvement-signal-wire";
import { isAmsReputationBridgeEnabled } from "./ams-reputation-bridge-wire";
import { isRagEnabled } from "./rag-wire";
import { isReputationEnabled } from "./reputation-wire";
import { isSafetyEnabled } from "./safety";
Expand Down Expand Up @@ -84,6 +85,7 @@ const FEATURE_GLOBAL_FLAG: Record<ConvergedFeatureKey, (env: Env) => boolean> =
e2eTests: isE2eTestGenerationEnabled,
screenshots: isScreenshotsEnabled,
improvementSignal: isImprovementSignalEnabled,
amsReputationBridge: isAmsReputationBridgeEnabled,
};

/** The named per-feature exceptions to `resolveConvergedFeature`'s default `"standard"` precedence — every
Expand Down
Loading
Loading