# p0147g: round-handler family decomposition via injected business services

- [Architecture] Three round-handler-unique business services extracted per the phase spec: `IFilterRoundBatcher` (token-budget batching), `IVerifyRoundCoordinator` (per-verifier dispatch + activation filter + tool surface), `IPlanOpenQuestionExtractor` (open-questions side-channel publish). One service per round-handler — collapsing batching + verifier-coordination + question-extraction into a shared "round logic service" would recreate the god-class one layer down. Each is mockable independently and registered Transient.
- [Architecture] Filter handler also gets a helper `FilterRoundCaller` for the per-batch / per-artifact SkillCallRequest construction and dispatch. Not in the spec's `new:` list, but the spec explicitly licenses extraction beyond the listed services when the handler would otherwise stay over 110 ("if a handler hits 110 after refactor, that's a signal another service is hiding inside"). `FilterRoundCaller` is a phase-local helper (not an interface) because nothing else needs to call it.
- [Architecture] Bootstrap handler gets two extracted helpers: `BootstrapPromptFactory` (static — pure formatting from role + repository + project map) and `BootstrapToolHostFactory` (transient — builds the bootstrap-restricted SandboxToolHost and returns a `BootstrapToolBundle` record exposing tools + change/decision accessors). Returning a bundle instead of the obsolete `SandboxToolHost` type keeps the CS0618 surface contained to the factory file via a single `#pragma warning disable` — same warning count as main, with the obsolete API now appearing in exactly one site we control.
- [Architecture] `IVerifyRoundCoordinator.RunRoundAsync` returns `VerifyRoundResult(int VerifierCount, IReadOnlyList<SkillObservation> Observations)` rather than a bare list. The handler differentiates "no active verifiers" (round not counted, ok skip) from "verifiers ran but emitted nothing" (round counted, ok). A bare list would have lost the first signal; a sentinel like `Count == 0` is ambiguous because verifiers can legitimately emit zero observations.
- [Architecture] `IPlanOpenQuestionExtractor.PublishSideChannel` returns the count of open questions, not a bool. Caller doesn't currently branch on it (the side-channel is fire-and-forget), but the count is the meaningful diagnostic for log lines without forcing the caller to re-inspect the plan.
- [Architecture] `ApiSkillRoundHandler` stayed on `SkillRoundHandlerBase`. It is already 36 lines and the base carries no round-specific business logic — it dispatches between two injected executors. The "no base class for round-specific business logic" principle targets _logic_ inheritance; routing-only template methods don't trigger it. Migrating the three handlers off the base would touch out-of-scope files (`SkillRoundHandler`, `SecuritySkillRoundHandler`) for no shrinkage benefit.
- [Implementation] `TokenBudgetBatcher` (static) collapsed into `FilterRoundBatcher` (`IFilterRoundBatcher`). Test file renamed `FilterRoundBatcherTests.cs`. No tradeoff — the static helper had one consumer; the interface now mirrors the per-handler business-service shape used elsewhere in the family.
- [Implementation] `BuildStructuredInput` / `MergeContext` from `GeneratePlanHandler` moved to a static `PlanContextRenderer.Merge` helper (pure formatting; convergence-result + consolidated-plan branching). Not promoted to a DI service — zero dependencies, deterministic, the inflection point at which "make it a class" pays off is non-zero deps or non-pure state.
- [TradeOff] Round handlers land in the 97-111 range, not at the 80-line target. Hard limit (120) honoured. The residual is orchestration mechanics: build prompt → dispatch via runtime → translate outcome → persist outputs. Extracting another layer to compress to 80 would buy code-shaped-prettiness at the cost of one more indirection the reader has to follow.
- [TradeOff] `VerifyRoundCoordinator` lands at 120 lines (right at the limit). The unavoidable bulk is the per-verifier SkillCallRequest construction. Could be cut further by routing through `ISkillRoundDispatcher` (the p0147d service) but that uses CurrentPhase-based phase mapping, and verify needs the fixed `SkillExecutionPhase.Verify`. Special-casing the dispatcher contract to accept an explicit phase would leak verify's quirk into the shared contract.
