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),ModelCatalogstatic class:CodexModels()/ClaudeModels()— spec-pinned static sets (no list command exists for either CLI).ParseGrokModels/ParseAgyModels— parsegrok models/agy modelsoutput (reusesAgyProbe.ParseModelsheader/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 viaIProcessRunner. 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 realIProcessRunner, moved here fromUsageGovernor.Cli/Program.cs(was a private nested class) soScanServicecan use it without Core depending on Cli. Program.cs'sCommandAgyProbenow resolves it viausing UsageGovernor.Core;(already present) — no behavior change there.src/UsageGovernor.Core/NoOpProcessRunner.cs(new) — safe no-opIProcessRunner;ScanServicedefaults to this so constructing one without explicitly opting in (every existing test) can never shell out. Production (Program.csCommandScanandApplyResetOutcome) now passes a realDefaultProcessRunner()explicitly.src/UsageGovernor.Core/GovernorState.cs— addedModels(List<ModelInfo>) toCodexProviderState,GrokProviderState,AgyProviderState; added newClaudeProviderState(models-only) andProviderStates.Claude.src/UsageGovernor.Core/GovernorConfig.cs— addedGrokExePath(default"grok", PATH resolution),AgyExePath(nullable override; null = existing agy-probe default location),ModelCatalogTimeoutMs(default 15s).src/UsageGovernor.Core/GovernorPaths.cs— added staticDefaultAgyExePath(the same%LocalAppData%\agy\bin\agy.execonventionCommandAgyProbealready used inline).src/UsageGovernor.Core/ScanService.cs—ScanServiceconstructor now takes an optionalIProcessRunner;ReadProviderscalls a newPopulateModelCatalogsstep (each provider wrapped in its own try/catch) that fillsCodex.Models/Claude.Modelsstatically and queriesGrok.Models/Agy.Modelslive via the injected runner.src/UsageGovernor.Cli/Program.cs—CommandScanandApplyResetOutcomenow constructScanServicewith a realDefaultProcessRunner()so production scans actually query the CLIs; removed the now-redundant privateDefaultProcessRunnerclass.- 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/agyModels; the 2-model grok case end-to-end throughScanService.Run; a failed grok query still lets the scan succeed withGrok.Modelsempty; 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 realgrok/agyprocesses. Production wiring inProgram.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/agywere not actually invoked for real duringdotnet 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.