- Status: done
- Date: 2026-06-05
- Specs touched: docs/specs/HEALTH.md (locus-B "Built" note), docs/specs/BRAIN_HOST_PROTOCOL.md (
GET /v1/health/systemdomain list)
Closes #40. Adds the reboot-required locus-B detector on the seam #34 built (health-system-report.md): host-agent stats Debian's /var/run/reboot-required flag (and /var/run/reboot-required.pkgs for the package list), the brain reconciles it under a new system report category. The first info-severity issue in the registry, and the last of the unblocked locus-B siblings off #34's GET /v1/health/system + ApplyFindings(category, …) seam (after clock-not-synced #39 and ram-pressure #38).
(#40 was labelled blocked on #34, which merged 2026-06-01 — the label was stale per the issue's own "drop the blocked label when it lands" instruction; dropped on pickup.)
Built on main. Unlike #38/#39, #40 was not a pure follow-up: the ram-pressure progress entry (ram-pressure-detector.md # What's next) flagged that reboot-required "needs a new HealthCategory protocol constant (none of storage|drives|services|resources|time fits a reboot flag), which is a locked-protocol decision to surface before building." That decision is taken here — see "Decisions surfaced" below. The change still lands additively: the brain's category-generic poller (pullSystemHealth) and ApplyFindings(category, …) reconcile need no change for a new category.
- Reporter (
internal/hostagent/rebootrequired/): stats/var/run/reboot-required; absent → nil (no reboot pending), present → one box-widereboot-requiredfinding. When present it also reads/var/run/reboot-required.pkgs, trims/dedupes/joins the package names into the finding'sDetails(the flag alone still raises — an unreadable/missing.pkgsyields emptyDetails, not a dropped finding). Stateless / cacheless like the ram-pressure reporter: astatis cheap and the 60s/v1/health/systempoll is well inside the spec's relaxed 1h cadence, so everyReadre-stats.parsePackages(split → trim → order-preserving dedupe) is a pure, unit-tested helper; paths are injectable for tests. Fails open — an unexpectedstaterror (notIsNotExist) logs and returns nil rather than raising on a tooling gap. - Wiring (
internal/hostagent/agent.go): a new consumer-sideRebootReporterinterface (sameRead() []protocol.Findingshape asRAMReporter/ClockReporter) + aRebootfield onAgent; thesystemHealthhandler emits thesystemcategory only when a reporter is wired (so a nil reporter reads as "not measured", never "no reboot pending").FakeRebootReporteradded tofake.gofor tests. - Protocol (
internal/protocol/host.go): newHealthCategorySystem = "system"constant on theHealthCategoryenum, pinned inhost_test.go's wire-value guard. The enum doc note records thatsystemwas added post-#34 for this detector's locus-B reclassification (DECISIONS.md2026-05-31). - Brain registry (
internal/health/health.go): a newSeverityInfo = "info"severity (the firstinfoissue; ranks belowwarninginseverityRank) + onereboot-requiredDefinition— display Categorycapacity, severityinfo, tier 2, no block flags,ReportCategory: system,Debounce: false. No reconcile-loop change. The summary string is authored plain-English ("A system update needs a reboot to finish.") in house style, matching how the sibling detectors reworded their dev-facing catalog text. - Real binary (
cmd/host-agent-real/main.go):a.Reboot = rebootrequired.New(). - Tests:
rebootrequiredReadtests against a real flag file written undert.TempDir(absent → healthy; present-with-.pkgs→ raises with the joined package list; present-without-.pkgs→ still raises, emptyDetails) + aparsePackagestrim/dedupe/order test; a host-agent handler test (systemcategory present + package list when wired, absent when not — plus the no-sources test now assertssystemabsent); a brain reconcile test (TestApplyFindings_RebootRequiredOneShot) proving 1-shot raise (no debounce gate), capacity/info surfacing,Detailscarry-through, and clear-on-absent; a definition-metadata test (TestList_RebootRequiredDefinition).
- HEALTH.md # Detector catalog (
reboot-requiredrow, locus B): presence of/var/run/reboot-required(+.pkgsfor the message), 1h cadence, raise on file present, clear on file absent (self-clears on reboot —/runis tmpfs). Realized; the locus-B "Built" note now lists it. (The 60s poll is a tighter floor than the 1h cadence; the spec cadence is the minimum freshness, not a throttle.) - HEALTH.md # Capacity & informational: display category
capacity,infoseverity, tier-2, all block flags false ("a quiet card"). Realized — firstinfoissue. - HEALTH.md # Cross-cutting detector policy: deterministic, non-noisy values are 1-shot (the policy's
version-mismatchcarve-out). File presence is deterministic — apt creates the flag, a reboot clears it via tmpfs/run— so it can't flap;Debounce: false. Realized. - BRAIN_HOST_PROTOCOL.md # Health findings report: the
GET /v1/health/systemdomain list now includessystem(andtime, which had been omitted), and the pending-reboot flag is named in the physical-detection examples.
These three are the design calls this issue forced — none were already pinned in a spec, so they're called out rather than buried:
- New
systemreport category. TheHealthCategoryaxis is strictly one-reporter-per-category for independent reconciliation; an OS-state flag fits none ofstorage|drives|services|resources|time.systemis the minimal addition and the brain poller is category-generic, so it costs one constant + one pin. This is the "locked-protocol decision" the ram-pressure entry flagged before building. SeverityInfois new. reboot-required is HEALTH.md's first implementedinfo-severity issue;health.Severitypreviously had only warning/error/critical. The addition is spec-grounded (HEALTH.md usesinfoforupdate-available/reboot-required) and mirrors thenotifypackage's existinginfoseverity.Debounce: false(1-shot). Unlike the three noisy locus-B siblings (service-down/clock/ram all debounce), a file-presence flag is deterministic and can't flap — so it raises on the first poll and surfaces the info banner one cycle sooner.
- Real-system verification is the maintainer's lane. The dev box has no
/var/run/reboot-required, soReadis tested against a real flag file written at a temp path. Exercising the live flag (e.g. afteraptinstalls a kernel/libc update, ortouch /var/run/reboot-required) on a real host / the QEMU medium lane is the confirming step.cmd/host-agent-realonly compiles withlibpam0g-devpresent (CI / medium lane), not on a bare dev box — the change there is an import + one assignment that satisfiesRebootReporterby construction. - No notify-center entry.
reboot-requiredsurfaces via the health banner (GET /api/v1/health) only — intentionally not in notify'shealthRulesallowlist, matching the reference locus-B detectors. Out of scope for #40 (detector-only). Aninfo-severity reboot notification could be a sensible follow-up once a "reboot now / schedule" action surface exists. - No remediation action. HEALTH.md's Tier-2 action is "reboot now / schedule"; this issue only raises the finding. The reboot/schedule control is a Settings → System surface, not yet built — the issue is the prerequisite, not the action.
- Settings → System surface that consumes this issue and offers the "reboot now / schedule" action (HEALTH.md # Capacity & informational, # Detector catalog) — frontend + a host-agent reboot op, not yet built.
- With
systemnow a real category, any future OS/box-state flag (e.g. a degraded-boot marker) plugs into the same category as a pure follow-up.