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
1 change: 1 addition & 0 deletions packages/loopover-engine/src/config-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const TOP_LEVEL_FIELDS = [
"sweepWatchdog",
"prReconciliation",
"activeReviewReconciliation",
"loopEscalation",
"federatedIntelligence",
] as const;

Expand Down
3 changes: 3 additions & 0 deletions packages/loopover-engine/src/focus-manifest-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
sweepWatchdogConfigToJson,
prReconciliationConfigToJson,
activeReviewReconciliationConfigToJson,
loopEscalationConfigToJson,
federatedIntelligenceConfigToJson,
settingsOverrideToJson,
type FocusManifest,
Expand Down Expand Up @@ -98,6 +99,8 @@ function focusManifestToNormalizedJson(manifest: FocusManifest): Record<string,
if (prReconciliation !== null) normalized.prReconciliation = prReconciliation;
const activeReviewReconciliation = activeReviewReconciliationConfigToJson(manifest.activeReviewReconciliation);
if (activeReviewReconciliation !== null) normalized.activeReviewReconciliation = activeReviewReconciliation;
const loopEscalation = loopEscalationConfigToJson(manifest.loopEscalation);
if (loopEscalation !== null) normalized.loopEscalation = loopEscalation;
const federatedIntelligence = federatedIntelligenceConfigToJson(manifest.federatedIntelligence);
if (federatedIntelligence !== null) normalized.federatedIntelligence = federatedIntelligence;

Expand Down
45 changes: 45 additions & 0 deletions packages/loopover-engine/src/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,18 @@ export type FocusManifestActiveReviewReconciliationConfig = {
enabled: boolean;
};

/**
* Config-as-code override for the fleet-wide Rent-a-Loop escalation sweep cron
* (LOOPOVER_LOOP_ESCALATION), declared under top-level `loopEscalation:` (#8018). Same shape and
* precedence as `prReconciliation:` above. The capability was never built when #6349 added the sweep,
* leaving it the only flag-gated cron in job-dispatch's switch without a manifest override.
* Not present ⇒ the caller falls back to the LOOPOVER_LOOP_ESCALATION env var.
*/
export type FocusManifestLoopEscalationConfig = {
present: boolean;
enabled: boolean;
};

/**
* Config-as-code opt-in for the federated fleet intelligence export (#1970), declared under
* `federatedIntelligence:`. Gates buildFederatedBundle (src/orb/federated-bundle.ts), which packages this
Expand Down Expand Up @@ -1217,6 +1229,7 @@ export type FocusManifest = {
sweepWatchdog: FocusManifestSweepWatchdogConfig;
prReconciliation: FocusManifestPrReconciliationConfig;
activeReviewReconciliation: FocusManifestActiveReviewReconciliationConfig;
loopEscalation: FocusManifestLoopEscalationConfig;
federatedIntelligence: FocusManifestFederatedIntelligenceConfig;
warnings: string[];
};
Expand Down Expand Up @@ -1402,6 +1415,11 @@ const EMPTY_ACTIVE_REVIEW_RECONCILIATION_CONFIG: FocusManifestActiveReviewReconc
enabled: false,
};

const EMPTY_LOOP_ESCALATION_CONFIG: FocusManifestLoopEscalationConfig = {
present: false,
enabled: false,
};

const EMPTY_FEDERATED_INTELLIGENCE_CONFIG: FocusManifestFederatedIntelligenceConfig = {
present: false,
enabled: false,
Expand Down Expand Up @@ -1437,6 +1455,7 @@ const EMPTY_MANIFEST: FocusManifest = {
sweepWatchdog: { ...EMPTY_SWEEP_WATCHDOG_CONFIG },
prReconciliation: { ...EMPTY_PR_RECONCILIATION_CONFIG },
activeReviewReconciliation: { ...EMPTY_ACTIVE_REVIEW_RECONCILIATION_CONFIG },
loopEscalation: { ...EMPTY_LOOP_ESCALATION_CONFIG },
federatedIntelligence: { ...EMPTY_FEDERATED_INTELLIGENCE_CONFIG },
warnings: [],
};
Expand Down Expand Up @@ -1478,6 +1497,7 @@ function emptyManifest(source: FocusManifestSource, warnings: string[] = []): Fo
sweepWatchdog: { ...EMPTY_SWEEP_WATCHDOG_CONFIG },
prReconciliation: { ...EMPTY_PR_RECONCILIATION_CONFIG },
activeReviewReconciliation: { ...EMPTY_ACTIVE_REVIEW_RECONCILIATION_CONFIG },
loopEscalation: { ...EMPTY_LOOP_ESCALATION_CONFIG },
federatedIntelligence: { ...EMPTY_FEDERATED_INTELLIGENCE_CONFIG },
};
}
Expand Down Expand Up @@ -2381,6 +2401,29 @@ export function activeReviewReconciliationConfigToJson(config: FocusManifestActi
return { enabled: config.enabled };
}

/**
* Parse the optional top-level `loopEscalation:` mapping (#8018). Mirrors
* {@link parsePrReconciliationConfig} exactly — `enabled` is the only field.
*/
function parseLoopEscalationConfig(value: JsonValue | undefined, warnings: string[]): FocusManifestLoopEscalationConfig {
if (value === undefined || value === null) return { ...EMPTY_LOOP_ESCALATION_CONFIG };
if (typeof value !== "object" || Array.isArray(value)) {
warnings.push('Manifest field "loopEscalation" must be a mapping; ignoring it.');
return { ...EMPTY_LOOP_ESCALATION_CONFIG };
}
const record = value as Record<string, JsonValue>;
const enabled = normalizeOptionalBoolean(record.enabled, "loopEscalation.enabled", warnings) ?? false;
return { present: true, enabled };
}

/** Serialize a loopEscalation config back into the parse-compatible shape so a cached snapshot
* round-trips through {@link parseLoopEscalationConfig} unchanged. Returns null when nothing is
* configured. */
export function loopEscalationConfigToJson(config: FocusManifestLoopEscalationConfig): JsonValue {
if (!config.present) return null;
return { enabled: config.enabled };
}

/**
* Parse the optional `federatedIntelligence:` mapping (#1970). Mirrors {@link parseUpstreamDriftIssuesConfig}
* exactly -- `enabled` is the only field, defaulting to false, so the parsed value IS the effective value and
Expand Down Expand Up @@ -3944,6 +3987,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
sweepWatchdog: parseSweepWatchdogConfig(record.sweepWatchdog, warnings),
prReconciliation: parsePrReconciliationConfig(record.prReconciliation, warnings),
activeReviewReconciliation: parseActiveReviewReconciliationConfig(record.activeReviewReconciliation, warnings),
loopEscalation: parseLoopEscalationConfig(record.loopEscalation, warnings),
federatedIntelligence: parseFederatedIntelligenceConfig(record.federatedIntelligence, warnings),
warnings,
};
Expand Down Expand Up @@ -3971,6 +4015,7 @@ export function parseFocusManifest(raw: unknown, source?: FocusManifestSource):
!manifest.sweepWatchdog.present &&
!manifest.prReconciliation.present &&
!manifest.activeReviewReconciliation.present &&
!manifest.loopEscalation.present &&
!manifest.federatedIntelligence.present
) {
warnings.push("Manifest contained no recognized focus fields; falling back to deterministic signals.");
Expand Down
14 changes: 9 additions & 5 deletions src/queue/job-dispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { executeAgentRun } from "../services/agent-orchestrator";
import { deliverNotification, evaluateNotificationEvent } from "../notifications/service";
import { isOpsEnabled, resolveOpsManifestOverride, runOpsAlerts } from "../review/ops-wire";
import { isSweepWatchdogEnabled, resolveSweepWatchdogManifestOverride, runSweepLivenessWatchdog } from "../review/sweep-watchdog";
import { isLoopEscalationSweepEnabled, runLoopEscalationSweep } from "../review/loop-escalation-wire";
import { isLoopEscalationSweepEnabled, resolveLoopEscalationManifestOverride, runLoopEscalationSweep } from "../review/loop-escalation-wire";
import { isPrReconciliationEnabled, resolvePrReconciliationManifestOverride, runOpenPrReconciliation } from "../review/pr-reconciliation";
import { isActiveReviewReconciliationEnabled, resolveActiveReviewReconciliationManifestOverride, runActiveReviewReconciliation } from "../review/active-review-reconciliation";
import { isSelfTuneEnabled, runSelfTune } from "../review/selftune-wire";
Expand Down Expand Up @@ -315,10 +315,14 @@ export async function processJob(env: Env, message: JobMessage): Promise<void> {
}
return;
case "loop-escalation-sweep":
// Rent-a-Loop escalation (#6349, flag LOOPOVER_LOOP_ESCALATION). Defense-in-depth: the cron only
// ENQUEUES this when the flag is ON, but a stale in-flight job that lands after a flag-flip must still
// no-op. Fails safe internally — never throws into the queue.
if (isLoopEscalationSweepEnabled(env)) await runLoopEscalationSweep(env);
// Rent-a-Loop escalation (#6349, flag LOOPOVER_LOOP_ESCALATION, config-as-code override #8018).
// Defense-in-depth: the cron only ENQUEUES this when enabled, but a stale in-flight job that lands
// after a flag-flip (env OR manifest) must still no-op, so disabled does zero work here too. Fails
// safe internally — never throws into the queue.
{
const loopEscalationManifestOverride = await resolveLoopEscalationManifestOverride(env);
if (isLoopEscalationSweepEnabled(env, loopEscalationManifestOverride)) await runLoopEscalationSweep(env);
}
return;
case "reconcile-open-prs":
// Self-heal (flag LOOPOVER_PR_RECONCILIATION). Defense-in-depth: the cron only ENQUEUES this when
Expand Down
51 changes: 49 additions & 2 deletions src/review/loop-escalation-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,65 @@ import {
type FleetLoopRow,
} from "../../packages/loopover-engine/src/loop-fleet-summary";
import { countRecentAuditEventsForActorAndTarget, recordAuditEvent } from "../db/repositories";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import { resolveLoopOverSelfRepoFullName } from "../config/loopover-repo-focus-manifest";
import { errorMessage } from "../utils/json";

const ALLOWED_DISCORD_HOSTS = new Set(["discord.com", "discordapp.com"]);
const DEFAULT_COOLDOWN_MINUTES = 60;
const AUDIT_EVENT_TYPE = "loop_escalation_notification.discord";
const AUDIT_TARGET_KEY = "fleet:loop-escalation";

/** True when the scheduled fleet-escalation sweep is enabled. Default OFF. */
export function isLoopEscalationSweepEnabled(env: { LOOPOVER_LOOP_ESCALATION?: string | undefined }): boolean {
/** A manifest-sourced enable override (#8018) -- the top-level `loopEscalation` block of the loopover
* self-repo's `.loopover.yml` (see FocusManifestLoopEscalationConfig). `present: false` means "no override
* configured", not "disabled" -- the caller falls through to the env var. Mirrors PrReconciliationManifestOverride. */
export type LoopEscalationManifestOverride = { present: boolean; enabled: boolean };

/** True when the scheduled fleet-escalation sweep is enabled. Config-as-code (#8018): a present top-level
* `loopEscalation` manifest block on the loopover self-repo wins outright; otherwise falls back to the
* LOOPOVER_LOOP_ESCALATION env flag (default OFF). Flag-OFF (default) → the cron enqueues no sweep job and
* the queue processor no-ops on a stale in-flight one (defense-in-depth, mirrors isPrReconciliationEnabled). */
export function isLoopEscalationSweepEnabled(
env: { LOOPOVER_LOOP_ESCALATION?: string | undefined },
manifestOverride?: LoopEscalationManifestOverride | undefined,
): boolean {
if (manifestOverride?.present) return manifestOverride.enabled;
return /^(1|true|yes|on)$/i.test((env.LOOPOVER_LOOP_ESCALATION ?? "").trim());
}

// Short in-isolate TTL cache for resolveLoopEscalationManifestOverride, mirroring ops-wire.ts /
// pr-reconciliation.ts: fleet-wide self-repo override, single slot, 60s TTL.
const LOOP_ESCALATION_MANIFEST_OVERRIDE_CACHE_TTL_MS = 60_000;
let loopEscalationManifestOverrideCache: { override: LoopEscalationManifestOverride; at: number } | null = null;

/**
* Config-as-code override lookup (#8018): read the top-level `loopEscalation` block off the loopover
* self-repo's `.loopover.yml`. A manifest load failure degrades to `{ present: false }` so a hiccup can
* never accidentally enable or disable the sweep. `nowMs` defaults to `Date.now()` so callers need no
* change, while tests can pass a deterministic value to exercise the TTL precisely.
*/
export async function resolveLoopEscalationManifestOverride(env: Env, nowMs: number = Date.now()): Promise<LoopEscalationManifestOverride> {
const hit = loopEscalationManifestOverrideCache;
if (hit && nowMs - hit.at < LOOP_ESCALATION_MANIFEST_OVERRIDE_CACHE_TTL_MS) return hit.override;
try {
const manifest = await loadRepoFocusManifest(env, resolveLoopOverSelfRepoFullName(env));
const config = manifest.loopEscalation;
const override = { present: config.present, enabled: config.enabled };
loopEscalationManifestOverrideCache = { override, at: nowMs };
return override;
} catch (error) {
console.warn(JSON.stringify({ event: "loop_escalation_manifest_override_error", message: errorMessage(error).slice(0, 200) }));
const override = { present: false, enabled: false };
loopEscalationManifestOverrideCache = { override, at: nowMs };
return override;
}
}

/** Test-only: clears the cached override, mirroring clearPrReconciliationManifestOverrideCacheForTest. */
export function clearLoopEscalationManifestOverrideCacheForTest(): void {
loopEscalationManifestOverrideCache = null;
}

function envString(env: Env, name: string): string | undefined {
const fromEnv = (env as unknown as Record<string, unknown>)[name];
return typeof fromEnv === "string" && fromEnv.trim().length > 0 ? fromEnv.trim() : undefined;
Expand Down
3 changes: 2 additions & 1 deletion src/signals/focus-manifest-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { listSignalSnapshots, persistSignalSnapshot } from "../db/repositories";
import { mapWithConcurrency } from "../queue/map-with-concurrency";
import type { JsonValue } from "../types";
import { nowIso } from "../utils/json";
import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, sweepWatchdogConfigToJson, prReconciliationConfigToJson, activeReviewReconciliationConfigToJson, federatedIntelligenceConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest";
import { contentLaneConfigToJson, experimentalConfigToJson, featuresConfigToJson, gateConfigToJson, MAX_FOCUS_MANIFEST_BYTES, parseFocusManifest, parseFocusManifestContent, repoDocGenerationConfigToJson, reviewConfigToJson, reviewRecapConfigToJson, maintainerRecapConfigToJson, opsConfigToJson, publicStatsConfigToJson, fairnessAnalyticsConfigToJson, draftFlowConfigToJson, upstreamDriftIssuesConfigToJson, sweepWatchdogConfigToJson, prReconciliationConfigToJson, activeReviewReconciliationConfigToJson, loopEscalationConfigToJson, federatedIntelligenceConfigToJson, settingsOverrideToJson, type FocusManifest, type FocusManifestSource, type RepoReviewContext } from "./focus-manifest";
import { LOOPOVER_REPO_FOCUS_MANIFEST_YAML, resolveLoopOverSelfRepoFullName } from "../config/loopover-repo-focus-manifest";
import type { LocalManifestLoadResult } from "../selfhost/private-config";

Expand Down Expand Up @@ -335,6 +335,7 @@ function manifestToJson(manifest: FocusManifest): Record<string, JsonValue> {
sweepWatchdog: sweepWatchdogConfigToJson(manifest.sweepWatchdog),
prReconciliation: prReconciliationConfigToJson(manifest.prReconciliation),
activeReviewReconciliation: activeReviewReconciliationConfigToJson(manifest.activeReviewReconciliation),
loopEscalation: loopEscalationConfigToJson(manifest.loopEscalation),
federatedIntelligence: federatedIntelligenceConfigToJson(manifest.federatedIntelligence),
};
}
Expand Down
2 changes: 2 additions & 0 deletions src/signals/focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export {
sweepWatchdogConfigToJson,
prReconciliationConfigToJson,
activeReviewReconciliationConfigToJson,
loopEscalationConfigToJson,
federatedIntelligenceConfigToJson,
FEDERATED_COLLECTOR_MODES,
settingsOverrideToJson,
Expand Down Expand Up @@ -79,6 +80,7 @@ export {
type FocusManifestSweepWatchdogConfig,
type FocusManifestPrReconciliationConfig,
type FocusManifestActiveReviewReconciliationConfig,
type FocusManifestLoopEscalationConfig,
type FocusManifestFederatedIntelligenceConfig,
type FederatedCollectorMode,
type FocusManifestSettings,
Expand Down
4 changes: 4 additions & 0 deletions test/unit/focus-manifest-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ prReconciliation:
enabled: false
activeReviewReconciliation:
enabled: true
loopEscalation:
enabled: true
`,
});
expect(result.status).toBe("ok");
Expand All @@ -137,6 +139,7 @@ activeReviewReconciliation:
sweepWatchdog: { enabled: true },
prReconciliation: { enabled: false },
activeReviewReconciliation: { enabled: true },
loopEscalation: { enabled: true },
});
});

Expand All @@ -150,6 +153,7 @@ activeReviewReconciliation:
expect(result.normalized).not.toHaveProperty("sweepWatchdog");
expect(result.normalized).not.toHaveProperty("prReconciliation");
expect(result.normalized).not.toHaveProperty("activeReviewReconciliation");
expect(result.normalized).not.toHaveProperty("loopEscalation");
expect(result.normalized).not.toHaveProperty("federatedIntelligence");
});

Expand Down
50 changes: 50 additions & 0 deletions test/unit/focus-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
sweepWatchdogConfigToJson,
prReconciliationConfigToJson,
activeReviewReconciliationConfigToJson,
loopEscalationConfigToJson,
federatedIntelligenceConfigToJson,
settingsOverrideToJson,
type FocusManifest,
Expand Down Expand Up @@ -959,6 +960,7 @@ describe("compileFocusManifestPolicy", () => {
sweepWatchdog: { present: false, enabled: false, staleAfterMinutes: null },
prReconciliation: { present: false, enabled: false },
activeReviewReconciliation: { present: false, enabled: false },
loopEscalation: { present: false, enabled: false },
federatedIntelligence: { present: false, enabled: false, collectorUrl: null, collectorMode: null, peerKeys: [] },
warnings: [],
});
Expand Down Expand Up @@ -2320,6 +2322,54 @@ describe("parseFocusManifest gate config", () => {
});
});

describe("loopEscalation: (#8018, Rent-a-Loop escalation sweep config-as-code override)", () => {
it("defaults to fully disabled/absent when the key is omitted, and does not make the manifest present on its own", () => {
const m = parseFocusManifest({});
expect(m.loopEscalation).toEqual({ present: false, enabled: false });
expect(m.present).toBe(false);
});

it("treats an explicit null the same as an omitted key", () => {
expect(parseFocusManifest({ loopEscalation: null }).loopEscalation).toEqual({ present: false, enabled: false });
});

it("warns and falls back to the default when the value is a non-mapping type (string or array)", () => {
const asString = parseFocusManifest({ loopEscalation: "nope" as never });
expect(asString.loopEscalation.present).toBe(false);
expect(asString.warnings.some((w) => /"loopEscalation" must be a mapping/.test(w))).toBe(true);
const asArray = parseFocusManifest({ loopEscalation: ["nope"] as never });
expect(asArray.loopEscalation.present).toBe(false);
expect(asArray.warnings.some((w) => /"loopEscalation" must be a mapping/.test(w))).toBe(true);
});

it("parses enabled: true, making the manifest present", () => {
const m = parseFocusManifest({ loopEscalation: { enabled: true } });
expect(m.loopEscalation).toEqual({ present: true, enabled: true });
expect(m.present).toBe(true);
});

it("parses enabled: false explicitly, still marking the manifest present (present is a real override, off)", () => {
const m = parseFocusManifest({ loopEscalation: { enabled: false } });
expect(m.loopEscalation).toEqual({ present: true, enabled: false });
expect(m.present).toBe(true);
});

it("warns and defaults to false when enabled is a non-boolean value", () => {
const m = parseFocusManifest({ loopEscalation: { enabled: "yes" as unknown as boolean } });
expect(m.loopEscalation.enabled).toBe(false);
expect(m.warnings.some((w) => /loopEscalation\.enabled/.test(w))).toBe(true);
});

it("round-trips through loopEscalationConfigToJson → parseFocusManifest unchanged", () => {
const m = parseFocusManifest({ loopEscalation: { enabled: true } });
expect(parseFocusManifest({ loopEscalation: loopEscalationConfigToJson(m.loopEscalation) }).loopEscalation).toEqual(m.loopEscalation);
});

it("loopEscalationConfigToJson returns null for an absent config", () => {
expect(loopEscalationConfigToJson(parseFocusManifest(null).loopEscalation)).toBeNull();
});
});

describe("federatedIntelligence: (#1970, opt-in federated fleet intelligence export config-as-code toggle)", () => {
it("defaults to fully disabled/absent when the key is omitted, and does not make the manifest present on its own", () => {
const m = parseFocusManifest({});
Expand Down
Loading