Skip to content

FLT-149 build report — usage-governor (per-sibling model catalog, governor state)

Branch: wt/WT-6022/FLT-149, off master @ d977619 (FLT-146/147). Worktree: C:/Users/fives/source/repos/ug-flt149

What changed

  • src/UsageGovernor.Core/ModelCatalog.cs (new) — ModelInfo (id/label/role/available), ModelRoles (hard/hard-light/cheap/fallback/claude-reserve/burst-overflow/adversarial), ModelCatalog static class:
  • CodexModels() / ClaudeModels() — spec-pinned static sets (no list command exists for either CLI).
  • ParseGrokModels / ParseAgyModels — parse grok models / agy models output (reuses AgyProbe.ParseModels header/bullet stripping) into role-tagged entries; loops over however many labels the CLI reports, so N models need zero code change.
  • QueryGrok / QueryAgy — timeout-guarded, fail-soft live query via IProcessRunner. Any non-zero exit, timeout, or thrown exception yields an empty list, never a crash and never a fabricated "available" model.
  • Slugify — label -> stable lowercase-hyphenated id.
  • src/UsageGovernor.Core/DefaultProcessRunner.cs (new) — the real IProcessRunner, moved here from UsageGovernor.Cli/Program.cs (was a private nested class) so ScanService can use it without Core depending on Cli. Program.cs's CommandAgyProbe now resolves it via using UsageGovernor.Core; (already present) — no behavior change there.
  • src/UsageGovernor.Core/NoOpProcessRunner.cs (new) — safe no-op IProcessRunner; ScanService defaults to this so constructing one without explicitly opting in (every existing test) can never shell out. Production (Program.cs CommandScan and ApplyResetOutcome) now passes a real DefaultProcessRunner() explicitly.
  • src/UsageGovernor.Core/GovernorState.cs — added Models (List<ModelInfo>) to CodexProviderState, GrokProviderState, AgyProviderState; added new ClaudeProviderState (models-only) and ProviderStates.Claude.
  • src/UsageGovernor.Core/GovernorConfig.cs — added GrokExePath (default "grok", PATH resolution), AgyExePath (nullable override; null = existing agy-probe default location), ModelCatalogTimeoutMs (default 15s).
  • src/UsageGovernor.Core/GovernorPaths.cs — added static DefaultAgyExePath (the same %LocalAppData%\agy\bin\agy.exe convention CommandAgyProbe already used inline).
  • src/UsageGovernor.Core/ScanService.cs — ScanService constructor now takes an optional IProcessRunner; ReadProviders calls a new PopulateModelCatalogs step (each provider wrapped in its own try/catch) that fills Codex.Models/Claude.Models statically and queries Grok.Models/Agy.Models live via the injected runner.
  • src/UsageGovernor.Cli/Program.cs — CommandScan and ApplyResetOutcome now construct ScanService with a real DefaultProcessRunner() so production scans actually query the CLIs; removed the now-redundant private DefaultProcessRunner class.
  • Tests: tests/UsageGovernor.Tests/ModelCatalogTests.cs (new, 15 tests) — static-set shape, grok/agy parsing + role mapping incl. the explicit "2-model grok output -> 2 models, zero schema change" case, Slugify, and QueryGrok/QueryAgy timeout-arg-passthrough + fail-soft (non-zero exit and thrown exception) behavior. tests/UsageGovernor.Tests/ScanServiceTests.cs — 6 new tests: default no-op runner leaves static catalogs populated and live catalogs empty; an injected fake runner populates grok/agy Models; the 2-model grok case end-to-end through ScanService.Run; a failed grok query still lets the scan succeed with Grok.Models empty; a throwing runner doesn't crash the scan.

Gate: dotnet build -warnaserror

Build succeeded.
    0 Warning(s)
    0 Error(s)

Gate: dotnet test

Passed!  - Failed:     0, Passed:   560, Skipped:     0, Total:   560, Duration: 18 s - UsageGovernor.Tests.dll (net10.0)

(560 = the full existing suite + all new FLT-149 tests; all green, no skips.)

Notes / honesty caveats

  • ScanService's default constructor (no runner passed) never shells out — this is deliberate so the pre-existing test suite doesn't start spawning real grok/agy processes. Production wiring in Program.cs (scan, and the reset-restore re-scan) explicitly passes the real runner.
  • Codex and Claude catalogs are static by design (spec-pinned): neither CLI has a model-list command, so "honest" here means "the fixed set the spec pins," not a live probe.
  • Grok/agy catalogs are live-queried every scan; a hung/absent CLI degrades to an empty list (not a stale carry-forward) — chosen over "last-known" because an empty list can't be mistaken for a currently-available model, and the spec explicitly allows either.
  • Not run as part of this gate: grok/agy were not actually invoked for real during dotnet test (the no-op default guarantees that); the FakeCatalogRunner-based tests substitute for a live round-trip against the real CLIs, which is out of scope for a unit-test gate.