Context
src/review/sweep-watchdog.ts implements the fleet-wide sweep-liveness watchdog (flag-gated by LOOPOVER_SWEEP_WATCHDOG): it detects when the scheduled regate sweep has stopped advancing a repo's last_regated_at marker and re-enqueues a targeted regate job.
The watchdog's enabled/disabled state is already full config-as-code: isSweepWatchdogEnabled() (src/review/sweep-watchdog.ts:30-36) reads a top-level sweepWatchdog: block from .loopover.yml (SweepWatchdogManifestOverride, parsed by parseSweepWatchdogConfig / typed as FocusManifestSweepWatchdogConfig in packages/loopover-engine/src/focus-manifest.ts) before falling back to the LOOPOVER_SWEEP_WATCHDOG env var.
However, the staleness threshold itself is a bare, unconfigurable constant:
// src/review/sweep-watchdog.ts:80
export const SWEEP_STALENESS_THRESHOLD_MS = 45 * 60 * 1000;
// src/review/sweep-watchdog.ts:82-87
export function isSweepStale(input: { openPullRequestCount: number; lastRegatedAt: string | null; nowMs: number }): boolean {
if (input.openPullRequestCount === 0) return false;
const lastMs = input.lastRegatedAt ? Date.parse(input.lastRegatedAt) : NaN;
if (!Number.isFinite(lastMs)) return true;
return input.nowMs - lastMs > SWEEP_STALENESS_THRESHOLD_MS;
}
isSweepStale has exactly one call site (src/review/sweep-watchdog.ts:161) and there is no code path anywhere that lets an operator change the 45-minute window — a self-hoster whose sweep cadence or repo count makes 45 minutes too tight (false-positive nudges) or too loose (slower incident detection) has no way to tune it short of editing this constant and redeploying. The existing sweepWatchdog: manifest block (top-level in .loopover.yml, same shape as ops:/publicStats:) is the natural, already-established place for this: it currently parses only { enabled: boolean } (see parseSweepWatchdogConfig in packages/loopover-engine/src/focus-manifest.ts), with no threshold field at all.
Requirements
- Add an optional
staleAfterMinutes (or equivalently named) numeric field to the sweepWatchdog: top-level manifest block, parsed alongside the existing enabled field in parseSweepWatchdogConfig (packages/loopover-engine/src/focus-manifest.ts).
SweepWatchdogManifestOverride (src/review/sweep-watchdog.ts) and FocusManifestSweepWatchdogConfig (packages/loopover-engine/src/focus-manifest.ts) must both carry the new field through their existing present/enabled shape (e.g. { present: boolean; enabled: boolean; staleAfterMinutes: number | null }), matching the sparse-override pattern already used elsewhere in this file (pickOverlayNullable).
isSweepStale (src/review/sweep-watchdog.ts:82) must accept an optional threshold override parameter (milliseconds) that, when provided, replaces SWEEP_STALENESS_THRESHOLD_MS in its comparison. SWEEP_STALENESS_THRESHOLD_MS remains the exported default used when no override is configured — do not remove it.
- The manifest-configured
staleAfterMinutes (converted to milliseconds) must be threaded from the config resolution point through to the isSweepStale call at src/review/sweep-watchdog.ts:161.
- Invalid input (non-numeric, zero, or negative
staleAfterMinutes) must be rejected by the parser with a warning pushed to the existing warnings array (mirroring how normalizeOptionalBoolean reports a bad enabled value in the same function), falling back to the default SWEEP_STALENESS_THRESHOLD_MS — never producing a zero/negative/NaN threshold at runtime.
- When
staleAfterMinutes is absent or the manifest block itself is absent (present: false), behavior must be byte-identical to today (45-minute default) — this is an additive, backward-compatible field, not a behavior change for repos that don't set it.
Deliverables
Test Coverage Requirements
This repo's Codecov patch gate is 99%+, hard, for src/**/packages/** — all touched files (packages/loopover-engine/src/focus-manifest.ts, src/review/sweep-watchdog.ts) fall under that gate. Add:
- Parser tests: valid
staleAfterMinutes, absent field (defaults to null/no-override), invalid values (string, zero, negative, NaN) each produce the documented warning and fall back safely.
isSweepStale tests: explicit override threshold changes the stale/not-stale boundary; omitted override preserves today's 45-minute (SWEEP_STALENESS_THRESHOLD_MS) behavior exactly (reuse the existing boundary-condition test style already in test/unit/sweep-watchdog.test.ts).
- Round-trip test:
sweepWatchdogConfigToJson → parseSweepWatchdogConfig preserves staleAfterMinutes (matching the existing round-trip coverage pattern for enabled).
Expected Outcome
A self-hoster can set sweepWatchdog: { enabled: true, staleAfterMinutes: 90 } in .loopover.yml to widen (or narrow) the sweep-liveness staleness window without editing code, exactly as they can already toggle enabled. Omitting staleAfterMinutes continues to use today's fixed 45-minute threshold with no behavior change.
Links & Resources
src/review/sweep-watchdog.ts (the module; SWEEP_STALENESS_THRESHOLD_MS at line 80, isSweepStale at line 82, call site at line 161)
packages/loopover-engine/src/focus-manifest.ts (FocusManifestSweepWatchdogConfig type, parseSweepWatchdogConfig, sweepWatchdogConfigToJson)
src/config/loopover-repo-focus-manifest.ts (bundled .loopover.yml fallback / documentation pattern for top-level fleet-wide config blocks like ops:/publicStats:)
test/unit/sweep-watchdog.test.ts (existing test file to extend)
Context
src/review/sweep-watchdog.tsimplements the fleet-wide sweep-liveness watchdog (flag-gated byLOOPOVER_SWEEP_WATCHDOG): it detects when the scheduled regate sweep has stopped advancing a repo'slast_regated_atmarker and re-enqueues a targeted regate job.The watchdog's
enabled/disabledstate is already full config-as-code:isSweepWatchdogEnabled()(src/review/sweep-watchdog.ts:30-36) reads a top-levelsweepWatchdog:block from.loopover.yml(SweepWatchdogManifestOverride, parsed byparseSweepWatchdogConfig/ typed asFocusManifestSweepWatchdogConfiginpackages/loopover-engine/src/focus-manifest.ts) before falling back to theLOOPOVER_SWEEP_WATCHDOGenv var.However, the staleness threshold itself is a bare, unconfigurable constant:
isSweepStalehas exactly one call site (src/review/sweep-watchdog.ts:161) and there is no code path anywhere that lets an operator change the 45-minute window — a self-hoster whose sweep cadence or repo count makes 45 minutes too tight (false-positive nudges) or too loose (slower incident detection) has no way to tune it short of editing this constant and redeploying. The existingsweepWatchdog:manifest block (top-level in.loopover.yml, same shape asops:/publicStats:) is the natural, already-established place for this: it currently parses only{ enabled: boolean }(seeparseSweepWatchdogConfiginpackages/loopover-engine/src/focus-manifest.ts), with no threshold field at all.Requirements
staleAfterMinutes(or equivalently named) numeric field to thesweepWatchdog:top-level manifest block, parsed alongside the existingenabledfield inparseSweepWatchdogConfig(packages/loopover-engine/src/focus-manifest.ts).SweepWatchdogManifestOverride(src/review/sweep-watchdog.ts) andFocusManifestSweepWatchdogConfig(packages/loopover-engine/src/focus-manifest.ts) must both carry the new field through their existingpresent/enabledshape (e.g.{ present: boolean; enabled: boolean; staleAfterMinutes: number | null }), matching the sparse-override pattern already used elsewhere in this file (pickOverlayNullable).isSweepStale(src/review/sweep-watchdog.ts:82) must accept an optional threshold override parameter (milliseconds) that, when provided, replacesSWEEP_STALENESS_THRESHOLD_MSin its comparison.SWEEP_STALENESS_THRESHOLD_MSremains the exported default used when no override is configured — do not remove it.staleAfterMinutes(converted to milliseconds) must be threaded from the config resolution point through to theisSweepStalecall atsrc/review/sweep-watchdog.ts:161.staleAfterMinutes) must be rejected by the parser with a warning pushed to the existingwarningsarray (mirroring hownormalizeOptionalBooleanreports a badenabledvalue in the same function), falling back to the defaultSWEEP_STALENESS_THRESHOLD_MS— never producing a zero/negative/NaN threshold at runtime.staleAfterMinutesis absent or the manifest block itself is absent (present: false), behavior must be byte-identical to today (45-minute default) — this is an additive, backward-compatible field, not a behavior change for repos that don't set it.Deliverables
staleAfterMinutesfield added toFocusManifestSweepWatchdogConfigand its parser/serializer (parseSweepWatchdogConfig,sweepWatchdogConfigToJson) inpackages/loopover-engine/src/focus-manifest.ts.SweepWatchdogManifestOverridetype and its consumption insrc/review/sweep-watchdog.tsupdated to carry the new field.isSweepStaleaccepts an optional threshold-override parameter; default behavior unchanged when omitted..loopover.ymlbundled fallback (src/config/loopover-repo-focus-manifest.ts) and the repo-root.loopover.ymldoc comments updated to mention the new field where thesweepWatchdog:block is documented (or where it would be documented if not already present).Test Coverage Requirements
This repo's Codecov patch gate is 99%+, hard, for
src/**/packages/**— all touched files (packages/loopover-engine/src/focus-manifest.ts,src/review/sweep-watchdog.ts) fall under that gate. Add:staleAfterMinutes, absent field (defaults to null/no-override), invalid values (string, zero, negative, NaN) each produce the documented warning and fall back safely.isSweepStaletests: explicit override threshold changes the stale/not-stale boundary; omitted override preserves today's 45-minute (SWEEP_STALENESS_THRESHOLD_MS) behavior exactly (reuse the existing boundary-condition test style already intest/unit/sweep-watchdog.test.ts).sweepWatchdogConfigToJson→parseSweepWatchdogConfigpreservesstaleAfterMinutes(matching the existing round-trip coverage pattern forenabled).Expected Outcome
A self-hoster can set
sweepWatchdog: { enabled: true, staleAfterMinutes: 90 }in.loopover.ymlto widen (or narrow) the sweep-liveness staleness window without editing code, exactly as they can already toggleenabled. OmittingstaleAfterMinutescontinues to use today's fixed 45-minute threshold with no behavior change.Links & Resources
src/review/sweep-watchdog.ts(the module;SWEEP_STALENESS_THRESHOLD_MSat line 80,isSweepStaleat line 82, call site at line 161)packages/loopover-engine/src/focus-manifest.ts(FocusManifestSweepWatchdogConfigtype,parseSweepWatchdogConfig,sweepWatchdogConfigToJson)src/config/loopover-repo-focus-manifest.ts(bundled.loopover.ymlfallback / documentation pattern for top-level fleet-wide config blocks likeops:/publicStats:)test/unit/sweep-watchdog.test.ts(existing test file to extend)