diff --git a/.env.example b/.env.example index 94da9e7a33..2b72ef2c7c 100644 --- a/.env.example +++ b/.env.example @@ -25,6 +25,7 @@ # Case-insensitive, trimmed; stray commas ignored. # Example: GITTENSORY_REVIEW_REPOS="JSONbored/gittensory,JSONbored/awesome-claude" GITTENSORY_REVIEW_REPOS= +# LOOPOVER_REVIEW_REPOS= # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_REPOS above when both are set # Instance-wide write kill switch for the cloud→self-host parallel-run migration. When set to "dry-run" # (or "disabled"), EVERY GitHub write from this instance is suppressed regardless of per-repo settings — @@ -38,19 +39,23 @@ GITTENSORY_REVIEW_REPOS= # Safety scan: defangs untrusted PR title/body/diff (prompt-injection # neutralization) and scans the diff for leaked secrets (secret_leak blocker). GITTENSORY_REVIEW_SAFETY=false +# LOOPOVER_REVIEW_SAFETY=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_SAFETY above when both are set # Grounds the AI-reviewer prompt with the PR's finished CI status + the full # post-change content of the changed files, so claims are verified against reality. GITTENSORY_REVIEW_GROUNDING=false +# LOOPOVER_REVIEW_GROUNDING=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_GROUNDING above when both are set # Retrieval-augmented context: appends semantically related code/docs from the # self-host vector index to the reviewer prompt. Inert until Qdrant or sqlite vectors are populated. GITTENSORY_REVIEW_RAG=false +# LOOPOVER_REVIEW_RAG=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_RAG above when both are set # Review-enrichment service (REES): POSTs the PR diff/files to the external # enrichment service and splices any public-safe brief into the AI reviewer # prompt. Also requires REES_URL and the repo in GITTENSORY_REVIEW_REPOS. GITTENSORY_REVIEW_ENRICHMENT=false +# LOOPOVER_REVIEW_ENRICHMENT=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_ENRICHMENT above when both are set # REES_URL=https://enrichment.example.internal # REES_SHARED_SECRET= # bearer secret configured on the REES service # REES_TIMEOUT_MS=8000 # optional; minimum 1000, default 8000 @@ -100,32 +105,39 @@ GITTENSORY_REVIEW_ENRICHMENT=false # Submitter-reputation spend control (internal-only): downgrades new/burst/low-rep # submitters to a deterministic-only review. Never surfaced publicly. GITTENSORY_REVIEW_REPUTATION=false +# LOOPOVER_REVIEW_REPUTATION=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_REPUTATION above when both are set # Renders the public PR comment as one in-place unified comment instead of the # legacy multi-panel comment. OFF keeps the legacy comment byte-identical. GITTENSORY_REVIEW_UNIFIED_COMMENT=false +# LOOPOVER_REVIEW_UNIFIED_COMMENT=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_UNIFIED_COMMENT above when both are set # --- Global capabilities (NOT scoped by GITTENSORY_REVIEW_REPOS) ------------- # Observability (read-only): cron anomaly scan over the gate-block ledger emits # ops_anomaly logs, plus a bearer-gated GET /v1/internal/ops/stats aggregate. GITTENSORY_REVIEW_OPS=false +# LOOPOVER_REVIEW_OPS=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_OPS above when both are set # Self-improvement / auto-tune loop: computes tuning recommendations, shadow-soaks # strictly-tightening ones, and auto-promotes only after the soak passes. Tightening-only. GITTENSORY_REVIEW_SELFTUNE=false +# LOOPOVER_REVIEW_SELFTUNE=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_SELFTUNE above when both are set # Parity readiness (shadow, record-only): shadow-records each finalized gate # decision and serves a readiness report at GET /v1/internal/parity. Changes no behavior. GITTENSORY_REVIEW_PARITY_AUDIT=false +# LOOPOVER_REVIEW_PARITY_AUDIT=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_PARITY_AUDIT above when both are set # Content-review lane: routes content repos (curated lists, registries) through the # dedicated content lane (dedup, source-evidence, scope, registry grounding). GITTENSORY_REVIEW_CONTENT_LANE=false +# LOOPOVER_REVIEW_CONTENT_LANE=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_CONTENT_LANE above when both are set # Public draft-submission flow: enables the /v1/drafts endpoints (contributor draft # -> GitHub OAuth -> fork PR). OFF every draft endpoint 404s. Needs draft secrets below. GITTENSORY_REVIEW_DRAFT=false +# LOOPOVER_REVIEW_DRAFT=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_DRAFT above when both are set # ============================================================================= # 2. Secrets (names only — set with `wrangler secret put NAME`) diff --git a/.env.selfhost.example b/.env.selfhost.example index 2b01917618..f316728b71 100644 --- a/.env.selfhost.example +++ b/.env.selfhost.example @@ -73,11 +73,17 @@ REDIS_URL=redis://redis:6379 # ============================================================================= SELFHOST_DEPLOYMENT_MODE=dry-run GITTENSORY_REVIEW_REPOS=owner/repo +# LOOPOVER_REVIEW_REPOS=owner/repo # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_REPOS above when both are set GITTENSORY_REVIEW_UNIFIED_COMMENT=true +# LOOPOVER_REVIEW_UNIFIED_COMMENT=true # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_UNIFIED_COMMENT above when both are set GITTENSORY_REVIEW_SAFETY=true +# LOOPOVER_REVIEW_SAFETY=true # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_SAFETY above when both are set GITTENSORY_REVIEW_GROUNDING=true +# LOOPOVER_REVIEW_GROUNDING=true # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_GROUNDING above when both are set GITTENSORY_REVIEW_RAG=false +# LOOPOVER_REVIEW_RAG=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_RAG above when both are set GITTENSORY_REVIEW_ENRICHMENT=false +# LOOPOVER_REVIEW_ENRICHMENT=false # #5555 dual-read: preferred name; wins over GITTENSORY_REVIEW_ENRICHMENT above when both are set # ============================================================================= # 4. AI provider — pick ONE block below and uncomment it (deterministic-only review if you skip diff --git a/src/env.d.ts b/src/env.d.ts index d6509cb10f..cc36d7d08e 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -295,22 +295,30 @@ declare global { * (ONE in-place comment in the converged shape) instead of the legacy `buildPublicPrIntelligenceComment` * panel. Default OFF — unset/false keeps the legacy panel byte-identical. */ GITTENSORY_REVIEW_UNIFIED_COMMENT?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_UNIFIED_COMMENT above — wins when both are set. */ + LOOPOVER_REVIEW_UNIFIED_COMMENT?: string; /** Inline comments (#inline-comments): when truthy (AND the repo is in GITTENSORY_REVIEW_REPOS AND the repo's * `.gittensory.yml` sets `review.inline_comments: true`), the AI reviewer ALSO leaves quiet, NON-BLOCKING * inline comments on specific changed lines, layered on top of the decision summary. Default OFF — * unset/false keeps the review path byte-identical (the model is never asked for inline findings). */ GITTENSORY_REVIEW_INLINE_COMMENTS?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_INLINE_COMMENTS above — wins when both are set. */ + LOOPOVER_REVIEW_INLINE_COMMENTS?: string; /** Fix-handoff blocks (#2176, config slice of #1962): when truthy (AND the repo is in GITTENSORY_REVIEW_REPOS * AND the repo's `.gittensory.yml` sets `review.fixHandoff: true`), a review finding is ALSO rendered as a * structured, machine-readable "apply this fix" block (src/review/fix-handoff-render.ts) for the * contributor's OWN local agent to consume — content only, no server-side write, no execution. Default * OFF — unset/false keeps the review path byte-identical (no block is ever built). */ GITTENSORY_REVIEW_FIX_HANDOFF?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_FIX_HANDOFF above — wins when both are set. */ + LOOPOVER_REVIEW_FIX_HANDOFF?: string; /** Convergence (safety): when truthy, the ported safety scan runs in the review path — (1) untrusted PR * title/body/diff is defanged (prompt-injection neutralized) before it reaches the AI reviewer, and (2) * the PR diff is scanned for leaked secrets, surfacing a `secret_leak` blocker. Default OFF — * unset/false keeps the review path byte-identical (no new branch is taken). */ GITTENSORY_REVIEW_SAFETY?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_SAFETY above — wins when both are set. */ + LOOPOVER_REVIEW_SAFETY?: string; /** Convergence (visual capture): when truthy, the review path captures a before/after screenshot for * PRs that touch WEB-VISIBLE files (frontend pages / public OG images — see review/visual/paths.ts * isVisualPath). "before" = production (PUBLIC_SITE_ORIGIN); "after" = the PR's preview deploy. Each shot @@ -324,21 +332,29 @@ declare global { * allowlist) — see review/visual-wire.ts screenshotsAllowed. Default OFF — unset/false captures nothing * (no render, no audit write, no comment change) so the review path is byte-identical to today. */ GITTENSORY_REVIEW_SCREENSHOTS?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_SCREENSHOTS above — wins when both are set. */ + LOOPOVER_REVIEW_SCREENSHOTS?: string; /** Convergence (grounding): when truthy, the AI reviewer prompt is GROUNDED — the PR's finished CI status * + the FULL post-change content of the changed files are appended so a non-frontier model verifies its * claims against reality instead of predicting CI / flagging symbols defined just outside the hunk. * Default OFF — unset/false keeps the reviewer prompt byte-identical and makes no extra GitHub fetch. */ GITTENSORY_REVIEW_GROUNDING?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_GROUNDING above — wins when both are set. */ + LOOPOVER_REVIEW_GROUNDING?: string; /** Convergence (e2eTests, #4190/#4189): master kill-switch for the opt-in, maintainer-triggered AI-generated * E2E test coverage feature. Default OFF — unset/false the feature is never active for any repo regardless * of a per-repo `features.e2eTests` override. */ GITTENSORY_REVIEW_E2E_TESTS?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_E2E_TESTS above — wins when both are set. */ + LOOPOVER_REVIEW_E2E_TESTS?: string; /** Convergence (improvementSignal, #4738, foundation phase of the #4737 epic): master kill-switch for the * read-only, ADVISORY PR quality-delta signal (the positive-axis counterpart to slop.ts's risk score). * This is config-as-code activation ONLY — no tier reads this flag yet; sibling sub-issues (#4739-#4746) * 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. */ GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL above — wins when both are set. */ + LOOPOVER_REVIEW_IMPROVEMENT_SIGNAL?: string; /** #one-shot-review-cadence: the operator's FLEET-WIDE default for AI review re-trigger cadence, consulted * only when a repo's `.gittensory.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 @@ -348,6 +364,8 @@ declare global { * generation, for operators who prefer that over one-shot. Never affects the deterministic gate (CI * status, mergeability, static-rule blockers), which always re-evaluates regardless of this flag. */ GITTENSORY_REVIEW_CONTINUOUS?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_CONTINUOUS above — wins when both are set. */ + LOOPOVER_REVIEW_CONTINUOUS?: string; /** Convergence (reputation): when truthy, the INTERNAL-only ported submitter-reputation signal extends the * AI-spend gate — a new / burst / low-reputation submitter is downgraded to a deterministic-only review * (the AI neurons are skipped), and the per-(project, submitter) outcome is recorded after the gate @@ -355,6 +373,8 @@ declare global { * unset/false reads NO reputation, records NOTHING, and leaves the AI-spend gate byte-identical (the new * branch is unreachable when off). */ GITTENSORY_REVIEW_REPUTATION?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_REPUTATION above — wins when both are set. */ + LOOPOVER_REVIEW_REPUTATION?: string; /** Convergence (ops / observability): when truthy, gittensory's OWN review-outcome data drives two * operator surfaces — (1) on the cron tick, an anomaly scan over the gate-block ledger + recommendation / * slop calibration emits a structured `ops_anomaly` log when something drifts (gate false-positive spike, @@ -364,6 +384,8 @@ declare global { * byte-identical to today. NOTE: this is read-only OBSERVABILITY only; the auto-tune / config-mutation * self-improve loop (src/review/auto-apply.ts) is deliberately NOT wired here — see ops-wire.ts. */ GITTENSORY_REVIEW_OPS?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_OPS above — wins when both are set. */ + LOOPOVER_REVIEW_OPS?: string; /** Self-heal: when truthy, an hourly watchdog scans the SAME acting-autonomy repo set the scheduled regate * sweep covers for a repo whose sweep marker hasn't advanced despite having open PRs to regate, emits a * structured `sweep_liveness_stale` log (Sentry-visible), and re-enqueues a targeted `agent-regate-sweep` @@ -384,11 +406,15 @@ declare global { * unreachable when off). Even when ON, retrieval is INERT until the self-host vector index is populated for * the repo (a cold/missing index degrades to no context). */ GITTENSORY_REVIEW_RAG?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_RAG above — wins when both are set. */ + LOOPOVER_REVIEW_RAG?: string; /** Deterministic impact map (#2184, part of #1971): operator-level kill-switch, ANDed with the per-repo * `.gittensory.yml review.impact_map` opt-in (see review/impact-map-wire's isImpactMapEnabled / * shouldComputeImpactMap). Default OFF — unset/false performs NO symbol extraction, NO RAG query, and adds * NO comment/prompt section, byte-identical to today. */ GITTENSORY_REVIEW_IMPACT_MAP?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_IMPACT_MAP above — wins when both are set. */ + LOOPOVER_REVIEW_IMPACT_MAP?: string; /** Repo quality-culture profile (#2995): when truthy, the AI reviewer prompt gains an ADDITIVE "REPO * QUALITY-CULTURE PROFILE" reference block — typical merged-PR size + common accepted labels, derived * deterministically from this repo's OWN `recent_merged_pull_requests` history (see @@ -397,17 +423,23 @@ declare global { * OFF — unset/false performs NO extra D1 read and keeps the reviewer prompt byte-identical (the new branch * is unreachable when off). ADVISORY GROUNDING ONLY: never a gate/scoring input. */ GITTENSORY_REVIEW_CULTURE_PROFILE?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_CULTURE_PROFILE above — wins when both are set. */ + LOOPOVER_REVIEW_CULTURE_PROFILE?: string; /** Review memory (#2179, part of #1964): operator-level kill-switch for repeat-false-positive suppression, * ANDed with the per-repo `.gittensory.yml review.memory` opt-in (see review/review-memory-wire's * isReviewMemoryEnabled / shouldApplyReviewMemory). Default OFF — unset/false performs NO suppression- * store read and NO matching, byte-identical to today. ADVISORY-ONLY: never applied to gate blockers. */ GITTENSORY_REVIEW_MEMORY?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_MEMORY above — wins when both are set. */ + LOOPOVER_REVIEW_MEMORY?: string; /** Review-enrichment service (REES): when truthy, the self-host review engine POSTs the PR diff/files to * REES and splices any public-safe brief into the AI reviewer prompt. Requires REES_URL and the repo in * GITTENSORY_REVIEW_REPOS. REES_ANALYZERS is an optional exact comma-list; unset/"all"/"*" lets REES run its * full registry. REES_FORWARD_GITHUB_TOKEN defaults off and must be explicitly enabled before * GitHub read tokens are included in the REES request. REES_SHARED_SECRET is a bearer secret and must never be committed. */ GITTENSORY_REVIEW_ENRICHMENT?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_ENRICHMENT above — wins when both are set. */ + LOOPOVER_REVIEW_ENRICHMENT?: string; REES_URL?: string; REES_SHARED_SECRET?: string; REES_TIMEOUT_MS?: string; @@ -420,6 +452,8 @@ declare global { * gate disposition byte-identical. AI-FREE (pure structured-data adjudication), so independent of the AI * reviewer; a generic hard blocker (e.g. a committed secret) is always preserved over a surface "merge". */ GITTENSORY_REVIEW_CONTENT_LANE?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_CONTENT_LANE above — wins when both are set. */ + LOOPOVER_REVIEW_CONTENT_LANE?: string; /** Convergence (self-improve / auto-tune): when truthy, the ported self-improvement loop * (src/review/auto-tune.ts + auto-apply.ts) runs on the cron tick over gittensory's OWN review-outcome * data — it computes tuning recommendations, SHADOW-SOAKS any STRICTLY-TIGHTENING recommendation in the @@ -434,6 +468,8 @@ declare global { * recording are wired, reading a promoted override into the live gate is a noted follow-up that must not * risk loosening the gate. See src/review/selftune-wire.ts. */ GITTENSORY_REVIEW_SELFTUNE?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_SELFTUNE above — wins when both are set. */ + LOOPOVER_REVIEW_SELFTUNE?: string; /** Experimental `gittensor` plugin (the `experimental:` manifest block, first key): the operator-level * kill-switch for gittensory's original subnet mining-registry/scoring integration, now opt-in rather than * a core dependency. ANDed with the per-repo `.gittensory.yml experimental.gittensor` opt-in -- neither @@ -463,6 +499,8 @@ declare global { * through to the existing mention path, so the worker is byte-identical to today. Hosted planning is retired * with the Cloudflare AI binding; self-host can run planning through the configured AI provider. */ GITTENSORY_REVIEW_PLANNER?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_PLANNER above — wins when both are set. */ + LOOPOVER_REVIEW_PLANNER?: string; /** Proof of Power (#1059): when truthy, the unauthenticated `GET /v1/public/stats` endpoint serves the public * homepage counter — computed LIVE from the public review ledger behind a 60s cache, so it stays current as * new reviews land. Default OFF — unset/false 404s the endpoint, so the @@ -485,6 +523,8 @@ declare global { * /v1/drafts endpoints accept a contributor draft -> GitHub OAuth -> fork PR against the content repo. * Default OFF — unset/false makes every draft endpoint 404 and writes nothing (byte-identical worker). */ GITTENSORY_REVIEW_DRAFT?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_DRAFT above — wins when both are set. */ + LOOPOVER_REVIEW_DRAFT?: string; /** owner/repo the draft fork PR targets (defaults to the awesome-claude content repo when unset). */ DRAFT_PUBLIC_REPO?: string; /** Base branch the draft PR opens against (defaults to "main"). */ @@ -501,6 +541,8 @@ declare global { * the actual COMPARISON vs reviewbot's authoritative decisions needs reviewbot's rows in the SAME table, * written by the deploy-time dual-run shadow step (out of scope here). See src/review/parity-wire.ts. */ GITTENSORY_REVIEW_PARITY_AUDIT?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_PARITY_AUDIT above — wins when both are set. */ + LOOPOVER_REVIEW_PARITY_AUDIT?: string; /** Convergence (cutover): comma-separated allowlist of repo full-names ("owner/repo") that may run the * PER-PR converged review features (safety defang + secret-leak, grounding, RAG, reputation AI-skip/record, * unified comment). A feature activates for a repo ONLY IF its existing global flag is ON AND the repo is @@ -510,6 +552,8 @@ declare global { * deploy). The cron/endpoint flags (ops / selftune / parity / content-lane / draft) are NOT scoped by * this allowlist — they stay global. See src/review/cutover-gate.ts. */ GITTENSORY_REVIEW_REPOS?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_REPOS above — wins when both are set. */ + LOOPOVER_REVIEW_REPOS?: string; /** Duplicate-winner adjudication (#dup-winner): when truthy, a same-issue duplicate cluster of OPEN PRs * spares exactly ONE winner — the EARLIEST opened = the LOWEST PR number among the OPEN siblings — instead * of gate-blocking + auto-closing every sibling. Only the LOSERS get the `duplicate_pr_risk` blocker, the diff --git a/src/queue/processors.ts b/src/queue/processors.ts index e52a10b7a9..9eb4f0f802 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -626,6 +626,7 @@ import type { RepositorySettings, } from "../types"; import { sha256Hex } from "../utils/crypto"; +import { dualPrefixEnvFlag } from "../utils/env"; import { errorMessage, nowIso } from "../utils/json"; import { maybeSuggestMilestoneMatchForPr } from "../integrations/project-tracker-adapter"; @@ -6297,11 +6298,16 @@ export async function shouldRefreshFilesForPreMergeChecks( * GITTENSORY_REVIEW_CONTINUOUS default. Both unset ⇒ "one_shot" — see AutoReviewConfig["cadence"]'s own doc * comment for the full semantics. */ export function resolveAiReviewCadence( - env: { GITTENSORY_REVIEW_CONTINUOUS?: string | undefined }, + env: { + GITTENSORY_REVIEW_CONTINUOUS?: string | undefined; + LOOPOVER_REVIEW_CONTINUOUS?: string | undefined; + }, configuredCadence: AiReviewCadence | null, ): AiReviewCadence { if (configuredCadence !== null) return configuredCadence; - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_CONTINUOUS ?? "") ? "continuous" : "one_shot"; + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_CONTINUOUS") + ? "continuous" + : "one_shot"; } function shouldProcessPullRequestPublicSurface( diff --git a/src/review/content-lane/flag.ts b/src/review/content-lane/flag.ts index f5888e8731..f01e5601f3 100644 --- a/src/review/content-lane/flag.ts +++ b/src/review/content-lane/flag.ts @@ -10,18 +10,20 @@ // Flag-off, the host never reaches these modules, so the live behavior is byte-identical. At // cutover the host flips the flag and routes awesome-claude + metagraphed PRs through the lane. +import { dualPrefixEnvFlag } from "../../utils/env"; + /** Env subset the content lane reads. The full Env adds it via env.d.ts; this keeps the lane * testable without the whole binding (pass a plain object). */ export interface ContentLaneEnv { - /** When truthy ("1"/"true"/"on"/"yes"), the content lane is enabled. Default OFF. */ + /** When truthy ("1"/"true"/"on"/"yes"), the content lane is enabled. Default OFF. + * #4774 dual-read: LOOPOVER_REVIEW_CONTENT_LANE below wins over this legacy name when both are set. */ GITTENSORY_REVIEW_CONTENT_LANE?: string; + /** #4774: LOOPOVER_ companion for GITTENSORY_REVIEW_CONTENT_LANE above — wins when both are set. */ + LOOPOVER_REVIEW_CONTENT_LANE?: string; } -const TRUTHY = new Set(["1", "true", "on", "yes"]); - /** Is the content lane enabled? Default OFF — only a recognized truthy flag turns it on. */ export function isContentLaneEnabled(env: ContentLaneEnv | undefined | null): boolean { - const raw = env?.GITTENSORY_REVIEW_CONTENT_LANE; - if (typeof raw !== "string") return false; - return TRUTHY.has(raw.trim().toLowerCase()); + if (!env) return false; + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_CONTENT_LANE"); } diff --git a/src/review/cutover-gate.ts b/src/review/cutover-gate.ts index f277927b05..78137c0952 100644 --- a/src/review/cutover-gate.ts +++ b/src/review/cutover-gate.ts @@ -15,6 +15,8 @@ // Matching is case-insensitive exact match on the trimmed "owner/repo" (GitHub repo full-names are // case-insensitive). Empty entries between commas are ignored, so a trailing/stray comma is harmless. +import { dualPrefixEnvString } from "../utils/env"; + /** * True when `repoFullName` is in the GITTENSORY_REVIEW_REPOS allowlist (per-repo cutover gate). * @@ -26,10 +28,13 @@ * isConvergenceRepoAllowed(env, repo)`), so a feature runs only when BOTH the global flag is ON and the repo is * allowlisted. */ -export function isConvergenceRepoAllowed(env: { GITTENSORY_REVIEW_REPOS?: string | undefined }, repoFullName: string): boolean { +export function isConvergenceRepoAllowed( + env: { GITTENSORY_REVIEW_REPOS?: string | undefined; LOOPOVER_REVIEW_REPOS?: string | undefined }, + repoFullName: string, +): boolean { const target = repoFullName.trim().toLowerCase(); if (!target) return false; - const raw = env.GITTENSORY_REVIEW_REPOS ?? ""; + const raw = dualPrefixEnvString(env as unknown as Record, "REVIEW_REPOS") ?? ""; for (const entry of raw.split(",")) { const candidate = entry.trim().toLowerCase(); if (candidate && candidate === target) return true; @@ -45,10 +50,14 @@ export function isConvergenceRepoAllowed(env: { GITTENSORY_REVIEW_REPOS?: string * webhook (the brokered model leaves is_registered=0), so a maintainer's whole repo set is pre-indexed for * codebase-aware reviews instead of waiting for a cold first-PR index. */ -export function listConvergenceRepos(env: { GITTENSORY_REVIEW_REPOS?: string | undefined }): string[] { +export function listConvergenceRepos(env: { + GITTENSORY_REVIEW_REPOS?: string | undefined; + LOOPOVER_REVIEW_REPOS?: string | undefined; +}): string[] { const seen = new Set(); const out: string[] = []; - for (const entry of (env.GITTENSORY_REVIEW_REPOS ?? "").split(",")) { + const raw = dualPrefixEnvString(env as unknown as Record, "REVIEW_REPOS") ?? ""; + for (const entry of raw.split(",")) { const trimmed = entry.trim(); if (!trimmed) continue; const key = trimmed.toLowerCase(); diff --git a/src/review/e2e-test-gen-wire.ts b/src/review/e2e-test-gen-wire.ts index b5d10e4ef2..5a98d56aa8 100644 --- a/src/review/e2e-test-gen-wire.ts +++ b/src/review/e2e-test-gen-wire.ts @@ -9,8 +9,13 @@ // `./feature-activation`). Truthy follows the codebase convention (`/^(1|true|yes|on)$/i`, same as // isRagEnabled / isGroundingEnabled / isSafetyEnabled). +import { dualPrefixEnvFlag } from "../utils/env"; + /** True when E2E test generation is enabled at the deployment level. Flag-OFF (default) → the feature is * never active for any repo, regardless of a per-repo `features.e2eTests` override. */ -export function isE2eTestGenerationEnabled(env: { GITTENSORY_REVIEW_E2E_TESTS?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_E2E_TESTS ?? ""); +export function isE2eTestGenerationEnabled(env: { + GITTENSORY_REVIEW_E2E_TESTS?: string | undefined; + LOOPOVER_REVIEW_E2E_TESTS?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_E2E_TESTS"); } diff --git a/src/review/enrichment-wire.ts b/src/review/enrichment-wire.ts index 62de60c053..7f72192099 100644 --- a/src/review/enrichment-wire.ts +++ b/src/review/enrichment-wire.ts @@ -9,6 +9,7 @@ import { extractLinkedIssueNumbers, getIssue } from "../db/repositories"; import { sanitizePublicComment } from "../queue-intelligence"; import { incr, observe } from "../selfhost/metrics"; +import { dualPrefixEnvFlag } from "../utils/env"; import { neutralizePromptInjection } from "./prompt-injection"; import { REES_ANALYZER_NAMES, REES_ANALYZER_NAME_SET, type ReesAnalyzerName } from "./enrichment-analyzer-names"; import type { PullRequestFileRecord } from "../types"; @@ -27,6 +28,7 @@ export { REES_ANALYZER_NAMES, type ReesAnalyzerName } from "./enrichment-analyze interface EnrichmentEnv { GITTENSORY_REVIEW_ENRICHMENT?: string | undefined; + LOOPOVER_REVIEW_ENRICHMENT?: string | undefined; REES_URL?: string | undefined; REES_SHARED_SECRET?: string | undefined; REES_TIMEOUT_MS?: string | undefined; @@ -180,7 +182,7 @@ export function probeReesSecretAtStartup(env: Env): void { export function isEnrichmentEnabled(env: Env): boolean { const cfg = reesConfig(env); return ( - /^(1|true|yes|on)$/i.test(cfg.GITTENSORY_REVIEW_ENRICHMENT ?? "") && + dualPrefixEnvFlag(cfg as unknown as Record, "REVIEW_ENRICHMENT") && Boolean(cfg.REES_URL?.trim()) ); } diff --git a/src/review/fix-handoff.ts b/src/review/fix-handoff.ts index afde531d5c..2dc4acc7cd 100644 --- a/src/review/fix-handoff.ts +++ b/src/review/fix-handoff.ts @@ -9,11 +9,15 @@ // core this, and four sibling `review:`-block features, now delegate to. import { resolveManifestOnlyFeature } from "./feature-activation"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when the operator enabled fix-handoff globally. Flag-OFF (default) ⇒ the caller never emits fix-handoff * blocks. Truthy follows the codebase convention (same regex as isInlineCommentsEnabled). */ -export function isFixHandoffEnabled(env: { GITTENSORY_REVIEW_FIX_HANDOFF?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_FIX_HANDOFF ?? ""); +export function isFixHandoffEnabled(env: { + GITTENSORY_REVIEW_FIX_HANDOFF?: string | undefined; + LOOPOVER_REVIEW_FIX_HANDOFF?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_FIX_HANDOFF"); } /** PURE (#4099): should the reviewer emit fix-handoff blocks for this PR? (1) The operator's diff --git a/src/review/grounding-wire.ts b/src/review/grounding-wire.ts index 2bcc621416..9e938543ba 100644 --- a/src/review/grounding-wire.ts +++ b/src/review/grounding-wire.ts @@ -16,6 +16,7 @@ import { githubRateLimitAdmissionKeyForToken, PRODUCT_USER_AGENT, timeoutFetch, import { getCachedGroundingFileContent, putCachedGroundingFileContent, recordAuditEvent } from "../db/repositories"; import type { CheckSummaryRecord, PullRequestFileRecord } from "../types"; import { repoParts } from "../utils/json"; +import { dualPrefixEnvFlag } from "../utils/env"; import { incr } from "../selfhost/metrics"; import { isConvergenceRepoAllowed } from "./cutover-gate"; import { @@ -29,8 +30,11 @@ import { } from "./review-grounding"; /** True when grounding is enabled. Flag-OFF (default) → no grounding is gathered and the prompt is unchanged. */ -export function isGroundingEnabled(env: { GITTENSORY_REVIEW_GROUNDING?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_GROUNDING ?? ""); +export function isGroundingEnabled(env: { + GITTENSORY_REVIEW_GROUNDING?: string | undefined; + LOOPOVER_REVIEW_GROUNDING?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_GROUNDING"); } /** Historical compatibility helper for the removed AI CI-refutation path. Grounding still feeds CI/full-file truth diff --git a/src/review/impact-map-wire.ts b/src/review/impact-map-wire.ts index ea8fb1f9a1..11f3671e88 100644 --- a/src/review/impact-map-wire.ts +++ b/src/review/impact-map-wire.ts @@ -15,13 +15,17 @@ import { resolveManifestOnlyFeature } from "./feature-activation"; import type { ImpactMapEntry } from "./impact-map"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when impact-map computation is enabled at the operator level. Flag-OFF (default) → the caller takes * no new branch, so no symbol extraction, no RAG query, and no impact-map section is ever computed or * rendered. Truthy follows the codebase convention (`/^(1|true|yes|on)$/i`, same as isRagEnabled / * isGroundingEnabled / isSafetyEnabled). */ -export function isImpactMapEnabled(env: { GITTENSORY_REVIEW_IMPACT_MAP?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_IMPACT_MAP ?? ""); +export function isImpactMapEnabled(env: { + GITTENSORY_REVIEW_IMPACT_MAP?: string | undefined; + LOOPOVER_REVIEW_IMPACT_MAP?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_IMPACT_MAP"); } /** Resolve whether impact-map computation should run for THIS repo/PR: the operator's global env kill-switch diff --git a/src/review/improvement-signal-wire.ts b/src/review/improvement-signal-wire.ts index 97c2ad39e0..6545ac5f59 100644 --- a/src/review/improvement-signal-wire.ts +++ b/src/review/improvement-signal-wire.ts @@ -12,8 +12,13 @@ // `./feature-activation`). Truthy follows the codebase convention (`/^(1|true|yes|on)$/i`, same as // isRagEnabled / isE2eTestGenerationEnabled). +import { dualPrefixEnvFlag } from "../utils/env"; + /** True when the PR improvement signal is enabled at the deployment level. Flag-OFF (default) → the feature is * never active for any repo, regardless of a per-repo `features.improvementSignal` override. */ -export function isImprovementSignalEnabled(env: { GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL ?? ""); +export function isImprovementSignalEnabled(env: { + GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL?: string | undefined; + LOOPOVER_REVIEW_IMPROVEMENT_SIGNAL?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_IMPROVEMENT_SIGNAL"); } diff --git a/src/review/inline-comments.ts b/src/review/inline-comments.ts index 2baf0e770c..cf764e17a6 100644 --- a/src/review/inline-comments.ts +++ b/src/review/inline-comments.ts @@ -23,12 +23,16 @@ import type { ReviewFindingSeverity } from "../signals/focus-manifest"; import type { AgentActionMode } from "../settings/agent-execution"; import type { PullRequestFileRecord } from "../types"; import { errorMessage } from "../utils/json"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when the operator enabled inline comments globally. Flag-OFF (default) ⇒ the caller never asks the model * for inline findings, so this module is never reached. Truthy follows the codebase convention (same regex as * isUnifiedReviewCommentEnabled / isSafetyEnabled). */ -export function isInlineCommentsEnabled(env: { GITTENSORY_REVIEW_INLINE_COMMENTS?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_INLINE_COMMENTS ?? ""); +export function isInlineCommentsEnabled(env: { + GITTENSORY_REVIEW_INLINE_COMMENTS?: string | undefined; + LOOPOVER_REVIEW_INLINE_COMMENTS?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_INLINE_COMMENTS"); } /** PURE (#4099): should the reviewer be asked to emit line-anchored inline findings for this PR? (1) The diff --git a/src/review/ops-wire.ts b/src/review/ops-wire.ts index d4ba40837a..1c553b867d 100644 --- a/src/review/ops-wire.ts +++ b/src/review/ops-wire.ts @@ -38,11 +38,15 @@ import { loadGatePrecisionReport, type GatePrecisionReport } from "../services/g import { buildRepoOutcomeCalibration, type OutcomeCalibration } from "../services/outcome-calibration"; import { triggerPagerDutyIncident, type PagerDutySeverity } from "../services/notify-pagerduty"; import { errorMessage, nowIso } from "../utils/json"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when the ops observability surface is enabled. Flag-OFF (default) → every export below is a no-op / * 404. Truthy follows the codebase convention (`/^(1|true|yes|on)$/i`, same as isSafetyEnabled). */ -export function isOpsEnabled(env: { GITTENSORY_REVIEW_OPS?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_OPS ?? ""); +export function isOpsEnabled(env: { + GITTENSORY_REVIEW_OPS?: string | undefined; + LOOPOVER_REVIEW_OPS?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_OPS"); } // ── Anomaly thresholds (gittensory-native; conservative so a handful of samples never cries wolf) ────────── diff --git a/src/review/parity-wire.ts b/src/review/parity-wire.ts index 6adcb09325..9bba99a8fb 100644 --- a/src/review/parity-wire.ts +++ b/src/review/parity-wire.ts @@ -31,6 +31,7 @@ import { computeGateParity, isParityCutoverReady, type GateAction, type GatePari import type { GateCheckConclusion, GateCheckEvaluation } from "../rules/advisory"; import { isSelfHostedReviewRuntime } from "../selfhost/review-runtime"; import { errorMessage, nowIso } from "../utils/json"; +import { dualPrefixEnvFlag } from "../utils/env"; // Bounded reason-class codes evaluateGateCheckCore (rules/advisory.ts) attaches to a NEUTRAL evaluation's // `warnings`, in the same priority order as its own return branches. Kept here (not re-exported from @@ -61,8 +62,11 @@ export function neutralHoldReasonCode(gateEvaluation: Pick, "REVIEW_PARITY_AUDIT"); } /** The `source` discriminator this writer stamps on every row — the SHADOW side computeGateParity compares diff --git a/src/review/planner.ts b/src/review/planner.ts index 9563704f52..f6a06d3612 100644 --- a/src/review/planner.ts +++ b/src/review/planner.ts @@ -16,11 +16,15 @@ import { sanitizePublicComment } from "../github/commands"; import { AGENT_COMMAND_COMMENT_MARKER } from "../github/comments"; import { gittensoryFooter, type GittensoryFooterEnv } from "../github/footer"; import type { GitHubWebhookPayload } from "../types"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when the issue-planning command is enabled. Flag-OFF (default) → every export below is unreachable from * the webhook path. Truthy follows the codebase convention (`/^(1|true|yes|on)$/i`, same as isSelfTuneEnabled). */ -export function isPlannerEnabled(env: { GITTENSORY_REVIEW_PLANNER?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_PLANNER ?? ""); +export function isPlannerEnabled(env: { + GITTENSORY_REVIEW_PLANNER?: string | undefined; + LOOPOVER_REVIEW_PLANNER?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_PLANNER"); } /** Recognize a bare `@gittensory plan` mention (the rest of the line is ignored). Returns false for any other diff --git a/src/review/rag-wire.ts b/src/review/rag-wire.ts index ce4bc6e926..4da02d0efa 100644 --- a/src/review/rag-wire.ts +++ b/src/review/rag-wire.ts @@ -21,11 +21,15 @@ import { createReviewAdapters } from "./adapters"; import { type RagChunk, retrieveContextWithMetrics, upsertChunks } from "./rag"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when RAG retrieval is enabled. Flag-OFF (default) → the caller takes no new branch, so no retrieval is * performed and the reviewer prompt is unchanged. */ -export function isRagEnabled(env: { GITTENSORY_REVIEW_RAG?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_RAG ?? ""); +export function isRagEnabled(env: { + GITTENSORY_REVIEW_RAG?: string | undefined; + LOOPOVER_REVIEW_RAG?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_RAG"); } /** Cap on how many changed-file paths feed the query string — bounds the query length / embed cost. */ diff --git a/src/review/repo-culture-profile-wire.ts b/src/review/repo-culture-profile-wire.ts index 0d261f3c7e..927be19763 100644 --- a/src/review/repo-culture-profile-wire.ts +++ b/src/review/repo-culture-profile-wire.ts @@ -17,14 +17,18 @@ // ADVISORY GROUNDING ONLY (house rule + #2995 requirement): this NEVER becomes a gate/scoring input. It only // ever appends a reference-only block to the AI reviewer's USER prompt, exactly like the RAG/grounding/ // enrichment sections it sits alongside in `services/ai-review.ts`'s buildUserPrompt. +import { dualPrefixEnvFlag } from "../utils/env"; import { resolveManifestOnlyFeature } from "./feature-activation"; import { extractRepoCultureProfile, type RepoCultureProfile } from "./repo-culture-profile"; import { neutralizePromptInjection } from "./prompt-injection"; /** True when the culture-profile grounding capability is enabled at all. Flag-OFF (default) → the per-repo * override below is never even consulted (mirrors isRagEnabled / isGroundingEnabled / isReputationEnabled). */ -export function isRepoCultureProfileEnabled(env: { GITTENSORY_REVIEW_CULTURE_PROFILE?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_CULTURE_PROFILE ?? ""); +export function isRepoCultureProfileEnabled(env: { + GITTENSORY_REVIEW_CULTURE_PROFILE?: string | undefined; + LOOPOVER_REVIEW_CULTURE_PROFILE?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_CULTURE_PROFILE"); } /** Resolve whether culture-profile grounding should apply for THIS repo/PR: the operator's global env diff --git a/src/review/reputation-wire.ts b/src/review/reputation-wire.ts index 5db75d2fa2..6bb64141e3 100644 --- a/src/review/reputation-wire.ts +++ b/src/review/reputation-wire.ts @@ -25,10 +25,14 @@ import { type SubmissionOutcome, type SubmitterStats, } from "./submitter-reputation"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when the reputation signal is enabled. Flag-OFF (default) → every helper below is a no-op. */ -export function isReputationEnabled(env: { GITTENSORY_REVIEW_REPUTATION?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_REPUTATION ?? ""); +export function isReputationEnabled(env: { + GITTENSORY_REVIEW_REPUTATION?: string | undefined; + LOOPOVER_REVIEW_REPUTATION?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_REPUTATION"); } // ── Anti-abuse thresholds. GENERIC mechanism (not the gameable secret — they don't reveal any review diff --git a/src/review/review-memory-wire.ts b/src/review/review-memory-wire.ts index f3ef406cd9..c0263d855c 100644 --- a/src/review/review-memory-wire.ts +++ b/src/review/review-memory-wire.ts @@ -13,13 +13,17 @@ import { resolveManifestOnlyFeature } from "./feature-activation"; import { matchSuppressions, type ReviewMemoryFindingInput } from "./review-memory-match"; import type { AdvisoryFinding, ReviewSuppressionRecord } from "../types"; import { incr } from "../selfhost/metrics"; +import { dualPrefixEnvFlag } from "../utils/env"; /** True when repeat-false-positive suppression is enabled at the operator level. Flag-OFF (default) → the * caller takes no new branch, so no suppression-store read and no matcher call ever happens. Truthy follows * the codebase convention (`/^(1|true|yes|on)$/i`, same as isImpactMapEnabled / isRagEnabled / * isSafetyEnabled). */ -export function isReviewMemoryEnabled(env: { GITTENSORY_REVIEW_MEMORY?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_MEMORY ?? ""); +export function isReviewMemoryEnabled(env: { + GITTENSORY_REVIEW_MEMORY?: string | undefined; + LOOPOVER_REVIEW_MEMORY?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_MEMORY"); } /** Resolve whether review-memory suppression should apply for THIS repo/PR: the operator's global env diff --git a/src/review/safety.ts b/src/review/safety.ts index a213aae7b3..5e98cb0440 100644 --- a/src/review/safety.ts +++ b/src/review/safety.ts @@ -6,6 +6,7 @@ // convention (`/^(1|true|yes|on)$/i`, same as isUnifiedReviewCommentEnabled / isEnabled). import type { AdvisoryFinding } from "../types"; +import { dualPrefixEnvFlag } from "../utils/env"; import { neutralizePromptInjection, safeReviewTitle } from "./prompt-injection"; import { ADVISORY_ONLY_SECRET_KINDS, HARD_SECRET_KINDS } from "./secret-patterns"; import { scanDiffForSecretsWithLocations, type SecretScanLocationMatch } from "./secrets-scan"; @@ -24,8 +25,9 @@ import { scanDiffForSecretsWithLocations, type SecretScanLocationMatch } from ". /** True when the safety scan is enabled. Flag-OFF (default) → every helper below is a no-op pass-through. */ export function isSafetyEnabled(env: { GITTENSORY_REVIEW_SAFETY?: string | undefined; + LOOPOVER_REVIEW_SAFETY?: string | undefined; }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SAFETY ?? ""); + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_SAFETY"); } /** The untrusted, author-controlled fields fed to the AI reviewer. */ diff --git a/src/review/selftune-wire.ts b/src/review/selftune-wire.ts index 281cf93904..439393e4d1 100644 --- a/src/review/selftune-wire.ts +++ b/src/review/selftune-wire.ts @@ -39,14 +39,18 @@ import { isAgentConfigured } from "../settings/autonomy"; import { resolveRepositorySettings } from "../settings/repository-settings"; import { buildRepoOutcomeCalibration } from "../services/outcome-calibration"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; +import { dualPrefixEnvFlag } from "../utils/env"; import { errorMessage } from "../utils/json"; import { computeTuningRecommendations, type GateEvalReport, type GateEvalRow } from "./auto-tune"; import { runAutoApplyRecommendations, type StorageEnv } from "./auto-apply"; /** True when the self-improvement loop is enabled. Flag-OFF (default) → every export below is a no-op. Truthy * follows the codebase convention (`/^(1|true|yes|on)$/i`, same as isOpsEnabled / isReputationEnabled). */ -export function isSelfTuneEnabled(env: { GITTENSORY_REVIEW_SELFTUNE?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SELFTUNE ?? ""); +export function isSelfTuneEnabled(env: { + GITTENSORY_REVIEW_SELFTUNE?: string | undefined; + LOOPOVER_REVIEW_SELFTUNE?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_SELFTUNE"); } /** The project's base confidence floor the tightening direction is judged against IN THE SOAK. Gittensory has no diff --git a/src/review/unified-comment-bridge.ts b/src/review/unified-comment-bridge.ts index 00b8c26836..814fb0ab2b 100644 --- a/src/review/unified-comment-bridge.ts +++ b/src/review/unified-comment-bridge.ts @@ -28,6 +28,7 @@ import { VISUAL_REGRESSION_FINDING_CODE } from "./visual/visual-findings"; // importers of `PR_PANEL_COMMENT_MARKER` from this module keep working. The unified body MUST prepend this // verbatim or `createOrUpdatePrIntelligenceComment` posts a DUPLICATE instead of updating in place. import { PR_PANEL_COMMENT_MARKER } from "../github/comments"; +import { dualPrefixEnvFlag } from "../utils/env"; import { LOOPOVER_GATE_CHECK_NAME } from "./check-names"; import { classifyChangedFile, type ReviewFileClass } from "./changed-files-classify"; import { githubPrFileDiffUrl } from "./changed-files-diff-link"; @@ -889,6 +890,9 @@ export function buildClosedUnifiedCommentBody(args: { repoFullName: string; pull } /** Truthy-env flag check, matching the codebase convention (e.g. SCORING_TIME_DECAY_ENABLED). */ -export function isUnifiedReviewCommentEnabled(env: { GITTENSORY_REVIEW_UNIFIED_COMMENT?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_UNIFIED_COMMENT ?? ""); +export function isUnifiedReviewCommentEnabled(env: { + GITTENSORY_REVIEW_UNIFIED_COMMENT?: string | undefined; + LOOPOVER_REVIEW_UNIFIED_COMMENT?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_UNIFIED_COMMENT"); } diff --git a/src/review/visual-wire.ts b/src/review/visual-wire.ts index 7600d7642d..b421f8b83b 100644 --- a/src/review/visual-wire.ts +++ b/src/review/visual-wire.ts @@ -18,7 +18,12 @@ // capture AFTER this key decides whether it is attempted for the repo at all — see resolveVisualCaptureConfig // in src/queue/processors.ts. That layer's existing force-off-only semantics are unchanged by #4616. +import { dualPrefixEnvFlag } from "../utils/env"; + /** True when the visual-capture global flag is enabled. Flag-OFF (default) → no capture is attempted. */ -export function isScreenshotsEnabled(env: { GITTENSORY_REVIEW_SCREENSHOTS?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SCREENSHOTS ?? ""); +export function isScreenshotsEnabled(env: { + GITTENSORY_REVIEW_SCREENSHOTS?: string | undefined; + LOOPOVER_REVIEW_SCREENSHOTS?: string | undefined; +}): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_SCREENSHOTS"); } diff --git a/src/services/draft.ts b/src/services/draft.ts index 1d12b1122f..9bae3c7d31 100644 --- a/src/services/draft.ts +++ b/src/services/draft.ts @@ -10,6 +10,7 @@ // reviewbot is collapsed into module constants + env vars. The flow is gated by GITTENSORY_REVIEW_DRAFT; when // the flag is off the router never mounts these handlers (callers see 404). import { decryptDraftToken, encryptDraftToken, newDraftId, randomDraftToken, sha256Hex, timingSafeEqualHex } from "../utils/crypto"; +import { dualPrefixEnvFlag } from "../utils/env"; import { timeoutFetch } from "../github/client"; const REDACT_KEYS = /(email|phone|address|contact|zip|postcode|name)/i; @@ -38,7 +39,7 @@ const SUPPORTED_CATEGORIES = [ // --------------------------------------------------------------------------- export function draftFlowEnabled(env: Env): boolean { - return /^(1|true|yes|on)$/i.test(String(env.GITTENSORY_REVIEW_DRAFT ?? "").trim()); + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_DRAFT"); } function draftConfig(env: Env): { publicRepo: string; baseRef: string; categories: string[]; branchPrefix: string } { diff --git a/src/settings/repository-settings.ts b/src/settings/repository-settings.ts index 70499dcdcd..b69382cfb2 100644 --- a/src/settings/repository-settings.ts +++ b/src/settings/repository-settings.ts @@ -3,11 +3,12 @@ import { loadOverride, type StorageEnv } from "../review/auto-apply"; import { resolveEffectiveSettings } from "../signals/focus-manifest"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; import type { RepositorySettings } from "../types"; +import { dualPrefixEnvFlag } from "../utils/env"; /** Default-OFF self-tune flag (mirrors selftune-wire's `isSelfTuneEnabled`; inlined here to avoid a * selftune-wire → repository-settings → selftune-wire import cycle). */ -function selfTuneFlagOn(env: { GITTENSORY_REVIEW_SELFTUNE?: string | undefined }): boolean { - return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SELFTUNE ?? ""); +function selfTuneFlagOn(env: { GITTENSORY_REVIEW_SELFTUNE?: string | undefined; LOOPOVER_REVIEW_SELFTUNE?: string | undefined }): boolean { + return dualPrefixEnvFlag(env as unknown as Record, "REVIEW_SELFTUNE"); } /** PURE: overlay a promoted (always TIGHTENING-only) self-tune override onto resolved settings. The auto-tune's