Firing the repository health scan

Hosting/RepoHealth can measure the fleet and cannot notice anything. This page is the design for the half that is missing, written down because it was designed and deliberately NOT built: a partly wired scheduler is worse than none, since it is an alarm that looks armed.

Status: NOT IMPLEMENTED. The classifier half of Plugins#2245 shipped (see /Hosting/ActionsBudgetAttribution); this half did not.

What was measured

Link in the chain State Evidence
Instrument ✅ exists RepoHealthProbe.Scan — reactive, credential-reusing, six checks
A surface to run it ✅ exists, human-only RepoHealthLayoutAreas declares Content (one recorded scan), Fleet (recorded scans merged) and Scan ("runs the probe NOW"). All three need a person to open a page
Schedule ❌ does not exist zero matches for schedul\|cron\|recurr\|Timer\|IHostedService\|BackgroundService\|Digest in RepoHealthContent.cs or RepoHealthLayoutAreas.cs
Anything that records a scan ❌ does not exist the Scan area renders what it finds and writes nothing
A scan ever recorded ❌ never, on either portal search nodeType:Hosting/RepoHealth partitions:allcount: 0 on memex.systemorph.com AND memex.meshweaver.cloud
Destination ✅ exists core's NotificationService platform bell, as /Hosting/FleetWatch already uses it
A sweep that could carry it ⚠️ exists for the wrong subject Hosting/FleetWatch sweeps Hosting/Deployment records and the workloads of their namespaces. It never mentions RepoHealth, and repositories are not deployments

So every link but the schedule and the record exists. That is the same shape FleetWatch closed for deployments, and the same conclusion: the destination costs nothing extra, and the missing piece is a join.

The shape to copy

FleetWatch's, exactly:

The finding category to route to the bell is actions-budget (MainBranchRules.BudgetCategory). It is the one condition that is (a) org-wide, (b) not about any one repository's code, and (c) actionable within minutes by a person with billing access. main-health is a candidate for a daily digest, never for a bell: a red trunk is usually already known to whoever made it red.

🚨 The registration hazard — the reason this is a design and not a diff

Where the hosted service is registered decides how many meshes run it. A hosted service registered in AddAI runs in EVERY test mesh in the fleet, and on the Orleans test cluster once per silo — a shape that has already cost one suite a 900 s kill with no verdict. An operational service that sweeps the world and writes a status node belongs to the MODULE, behind a feature switch, so that portals get it and a test mesh opts in.

The precedent to follow is in this repo, and it carries three separate lessons in eleven lines (SelfUpdateAksModuleAttribute):

  1. Register the singleton AND the hosted service, from the same instance.
    services.AddSingleton<RepoHealthSweep>();
    services.AddHostedService(sp => sp.GetRequiredService<RepoHealthSweep>());
    
    🚨 Registering the singleton without the AddHostedService line means it is never STARTED, and every case then waits out its bound on state nobody created — a hang with no error. And resolving the same instance (rather than AddHostedService<T>()) is what keeps the health check from watching a different deadline from the one the sweep arms.
  2. Do not host the guard inside the thing that can fail. FleetWatch's sweep is armed inside a NodeType's configuration lambda, and on 2026-09-16 that whole chain silently failed to load while the assembly's hosted services kept running. A guard hosted inside the thing that fails is unreachable exactly when it matters, so the heartbeat lives in the module's own registration.
  3. A monitoring outage must not restart a pod. The health check gets the census tag and no probe tag, so it prints its reading whether or not the sweep is beating — "I measured and it is fine" and "nothing here measures that" have to be different sentences.

A Features:* switch gates the registration; the module wrapper (IsolatedModuleHostedService) then makes a failure cost this feature and never the host.

The two decisions that are NOT measurements

Neither can be settled by reading anything, so neither was decided here:

  1. What a scan record is. A node per run, a node per day, or ONE rolling record per portal. Each is a different retention story and a different Fleet board:

    • per run — full history, and an unbounded node count in a partition nobody prunes;
    • per day — a bounded series, and a merge that has to decide what "today's scan" means across time zones (DisplayStamp already renders in the viewer's zone, so the KEY would have to be UTC while the display is not);
    • one rolling record — trivially bounded, no history at all, and Merge — which exists to keep the newest scan per repository — becomes nearly pointless.

    RepoHealthLayoutAreas.Merge and FleetQuery are written for a MULTI-record world (newest-wins per repository, anchored to one partition), which argues for per-day; but that is an argument from the existing code, not a decision about retention cost.

  2. Who is notified, and how loudly. The bell is read-scoped to hub.IsGlobalAdmin(), and the Notifications module escalates per recipient to email/Teams. An org budget refusal at 07:00 on a Saturday is either exactly what someone wants paged or exactly what makes them mute the bell.

What must NOT be done in the meantime

/Hosting/ActionsBudgetAttribution · /Hosting/FleetWatch · /Hosting/RequiredStatusChecks