unknownTopLevelWarnings (packages/loopover-engine/src/config-lint.ts ~80-97) exists but is wired only into the offline validator (loopover_validate_config, the validate route, the admin dry-run write, scripts/loopover-config-lint.ts). The runtime path — loadRepoFocusManifest → parseFocusManifestContent → parseFocusManifest (focus-manifest.ts ~4023) — never calls it. An operator editing the mounted file directly (our normal workflow) gets no check at all.
Worse, unknown-key detection is top-level only. Every nested parser reads known keys by name and never enumerates the supplied set, so settings.*, gate.*, features.*, review.* typos are invisible even to the offline linter. (Sole exception: review.enrichment.) The doc comment at focus-manifest.ts ~1999 claiming features unknown keys "are dropped with a warning" is factually wrong — ~2011-2013 iterates CONVERGED_FEATURE_KEYS and never looks at supplied keys.
Typos that silently disable a safety control
| Typo |
Silently disables |
settings.agentPause / agent_paused |
the per-repo kill switch |
settings.manualReviewLabel |
the manual-review hold — falls back to AGENT_LABEL_NEEDS_REVIEW, so your real label no longer matches and labelled PRs auto-merge |
settings.autoCloseExemptLogins |
auto-close exemptions — protected authors become closable |
settings.contributorBlacklist |
per-repo bans |
settings.hardGuardrailGlob (singular) |
your added guardrails (built-in floor survives) |
settings.contributorOpenPrCap |
the open-PR cap |
settings.accountAgeThresholdDays |
new-account protection |
settings.moderationRules |
moderation |
top-level gates: / Gate: |
the entire block |
any gate.* / review.* / features.* nested typo |
that knob, at any depth |
a malformed glob in hardGuardrailGlobs |
that entry — normalizeStringList does no glob validation, so it just never matches |
Compounding: malformed layers and warnings both vanish
combineConfigLayersWithMeta (src/selfhost/private-config.ts ~189-232) warns only for the shared base. If the global default or per-repo layer fails to parse (YAML typo, a file caught mid-vim-write, a truncated docker cp), it is dropped at ~202-204 with no warning, no log, no metric — that repo silently demotes to fleet house policy with a clean-looking manifest.
- Private-manifest
warnings reach no consumer: the only PR-comment consumer deliberately uses the public manifest (processors.ts ~11617, correctly, to avoid leaking private policy), and adminGetConfig scope "effective" (src/mcp/server.ts ~3995-4004) extracts only loaded.content and discards warnings — so loopover_admin_config_read --scope effective will happily show a config whose gate: block was silently dropped as a type error.
- Config-mount health is a one-shot boot snapshot (
src/server.ts ~403-409, ~916): if the mount is lost or emptied while running, the gauge stays pinned at healthy and every repo silently reverts to defaults — the exact incident src/selfhost/health.ts ~240-245 documents, caught only if it happens before boot.
Verified sound: I exercised mergeConfigOverlay against its documented contract — 8/8 edge cases pass (explicit null clears, arrays replace wholesale, nested maps merge key-by-key, __proto__ does not pollute). The merge core is not the problem.
Fix
- Call
unknownTopLevelWarnings from parseFocusManifestContent itself, and add per-block known-key sets for settings/gate/features/review.
- Warn for every dropped layer, carrying the candidate path.
- Log private-manifest warnings as a structured event + counter; return
warnings from adminGetConfig --scope effective.
- Make the config-dir health gauge a live callback, and add a per-repo "resolved to no local manifest" signal.
- Fix the false doc comment at
focus-manifest.ts ~1999.
unknownTopLevelWarnings(packages/loopover-engine/src/config-lint.ts~80-97) exists but is wired only into the offline validator (loopover_validate_config, the validate route, the admin dry-run write,scripts/loopover-config-lint.ts). The runtime path —loadRepoFocusManifest→parseFocusManifestContent→parseFocusManifest(focus-manifest.ts~4023) — never calls it. An operator editing the mounted file directly (our normal workflow) gets no check at all.Worse, unknown-key detection is top-level only. Every nested parser reads known keys by name and never enumerates the supplied set, so
settings.*,gate.*,features.*,review.*typos are invisible even to the offline linter. (Sole exception:review.enrichment.) The doc comment atfocus-manifest.ts~1999 claimingfeaturesunknown keys "are dropped with a warning" is factually wrong — ~2011-2013 iteratesCONVERGED_FEATURE_KEYSand never looks at supplied keys.Typos that silently disable a safety control
settings.agentPause/agent_pausedsettings.manualReviewLabelAGENT_LABEL_NEEDS_REVIEW, so your real label no longer matches and labelled PRs auto-mergesettings.autoCloseExemptLoginssettings.contributorBlacklistsettings.hardGuardrailGlob(singular)settings.contributorOpenPrCapsettings.accountAgeThresholdDayssettings.moderationRulesgates:/Gate:gate.*/review.*/features.*nested typohardGuardrailGlobsnormalizeStringListdoes no glob validation, so it just never matchesCompounding: malformed layers and warnings both vanish
combineConfigLayersWithMeta(src/selfhost/private-config.ts~189-232) warns only for the shared base. If the global default or per-repo layer fails to parse (YAML typo, a file caught mid-vim-write, a truncateddocker cp), it is dropped at ~202-204 with no warning, no log, no metric — that repo silently demotes to fleet house policy with a clean-looking manifest.warningsreach no consumer: the only PR-comment consumer deliberately uses the public manifest (processors.ts~11617, correctly, to avoid leaking private policy), andadminGetConfigscope"effective"(src/mcp/server.ts~3995-4004) extracts onlyloaded.contentand discardswarnings— soloopover_admin_config_read --scope effectivewill happily show a config whosegate:block was silently dropped as a type error.src/server.ts~403-409, ~916): if the mount is lost or emptied while running, the gauge stays pinned at healthy and every repo silently reverts to defaults — the exact incidentsrc/selfhost/health.ts~240-245 documents, caught only if it happens before boot.Verified sound: I exercised
mergeConfigOverlayagainst its documented contract — 8/8 edge cases pass (explicitnullclears, arrays replace wholesale, nested maps merge key-by-key,__proto__does not pollute). The merge core is not the problem.Fix
unknownTopLevelWarningsfromparseFocusManifestContentitself, and add per-block known-key sets forsettings/gate/features/review.warningsfromadminGetConfig --scope effective.focus-manifest.ts~1999.