Skip to content

fix(debate): route advocate + critic through the same persona→backend resolver as council - #19

Open
KSiig wants to merge 1 commit into
gabelul:mainfrom
KSiig:fix/debate-mode-cli-routing
Open

fix(debate): route advocate + critic through the same persona→backend resolver as council#19
KSiig wants to merge 1 commit into
gabelul:mainfrom
KSiig:fix/debate-mode-cli-routing

Conversation

@KSiig

@KSiig KSiig commented Aug 5, 2026

Copy link
Copy Markdown

What this PR fixes

Debate mode (consult({ mode: "debate" })) silently fails with Round 1 advocate failed: no api key resolved whenever a persona is configured to route through a CLI backend (the default for the bundled architect / critic, which carry no defaultModel and fall through to config.modes.solo.model). Council mode handles the same scenario correctly via resolveCouncilMembers; debate never got the equivalent routing.

The root cause: debate.ts:executeDebate resolved every persona through resolveAdvisor, which is inline-only. When a persona's intended route is a CLI backend, the registry lookup misses and getAuth (advisor.ts:50) returns the "no api key resolved" error that consult surfaces verbatim.

How

Extracts the per-persona resolver into a shared kernel (src/resolve-side.ts) so council and debate use the same inline-vs-CLI decision. Council wraps it with its pre-failure convention (one bad persona doesn't kill the council); debate calls it directly and bails on any failure (sequential rounds can't tolerate a missing seat).

  • src/resolve-side.ts — new resolveSide(persona, rawPersona, config, resolveAdvisorFn), returns a discriminated ResolvedSide (kind: "inline" | "cli"). Pure, registry-injected for testability.
  • src/council.tsresolveCouncilMembers becomes a thin loop over resolveSide (behavior identical, ~30 lines deleted). ResolvedMember is now an alias of ResolvedSide.
  • src/debate.ts — advocate + critic resolve through resolveSide. callStep dispatches on kind: inline → callAdvisor (existing path), CLI → callCliAdvisor (mirrors council.ts:runMember). Synthesizer stays inline-only to match council mode's existing behavior.
  • tests/debate-cli.test.ts — 9 new cases mirroring council-cli.test.ts: inline fallback for personas without defaultModel, preset CLI routing, custom CLI with declared/unknown window, persona-scoped backend precedence, same-model-different-routes.
  • SPEC.md (debate section + §B), README.md (debate row) — note that advocate/critic honour persona-scoped CLI routing identically to council seats.

Why this and not something simpler

  • Why not just patch resolveAdvisor to handle CLI? It doesn't know about persona-scoped backends or the legacy backends[modelKey] map — that's resolvePersonaBackend's job in config.ts. Lifting that into advisor.ts would cross a module boundary and pull CLI types into the registry abstraction.
  • Why not make the synthesizer CLI-aware too? Council mode also resolves the synthesizer inline-only (council.ts:104, never via resolveCouncilMembers). Adding CLI support to the synthesizer is a separate, larger change that affects both modes — out of scope here. Noted in debate.ts as a pre-existing shared constraint.
  • Why extract a new file instead of inlining in both modes? Council and debate need the same routing logic. A shared kernel eliminates the drift risk that caused this bug in the first place — the persona→backend wiring is now a single source of truth.

Test plan

  • cd packages/bpx-consult && npx tsc --noEmit — clean.
  • cd packages/bpx-consult && npx vitest run277/277 pass (was 268 + 9 new in debate-cli.test.ts).
  • cd packages/bpx-consult && npm pack --dry-runsrc/resolve-side.ts (3.5kB) included in tarball.

Risk assessment

Low. The change is additive on the council side (no behavior change — the refactor preserves resolveCouncilMembers's return shape and the 11 existing council-cli.test.ts cases still pass). On the debate side, the persona→backend routing is brand-new but the resolveSide helper itself is exercised by council's existing 11 test cases, so the kernel is well-covered; the 9 new debate-cli.test.ts cases guard the debate-specific wiring.

Backwards compatibility. The public API of executeDebate, DebateDetails, formatDebatePartial is unchanged. resolveCouncilMembers keeps its existing signature; only the implementation was simplified. Any consumer importing these from @booplex/bpx-consult is unaffected.

Open question (not blocking). The synthesizer's inline-only constraint is shared with council. If bpx-consult users have configured a CLI-backed synthesizer under modes.council.synthesizer, both council and debate silently fail today. Happy to follow up with a separate PR if the maintainers agree the constraint should be lifted.


🤖 Generated with pi. Co-authored-by: pi pi@anthropic.com

… resolver as council

Debate mode resolved every persona through `resolveAdvisor`, which is
inline-only — a CLI-backed persona (the default route for the bundled
architect/critic, which carry no `defaultModel` and fall through to
`config.modes.solo.model`) silently broke with "no api key resolved"
from `getAuth` (advisor.ts:50) before round 1 ran. Council mode handled
the same scenario correctly via `resolveCouncilMembers`; debate never
got the equivalent routing.

Extracts the per-persona resolver into `src/resolve-side.ts` so both
modes share the same inline-vs-CLI decision (council §1: persona-scoped
backend takes precedence over the legacy `backends[modelKey]` map).
Council wraps it with its pre-failure convention (one bad persona
mustn't kill the council); debate calls it directly and bails on any
failure (sequential rounds can't tolerate a missing seat). The
synthesizer stays inline-only to match council mode's existing
behavior — if a CLI-backed synthesizer is the intended route, that's a
pre-existing shared constraint, not a regression.

- src/resolve-side.ts — new shared kernel (resolveSide, ResolvedSide).
- src/council.ts — resolveCouncilMembers becomes a thin loop over
  resolveSide; ResolvedMember aliased to ResolvedSide.
- src/debate.ts — advocate/critic resolve through resolveSide;
  callStep dispatches on kind (inline → callAdvisor, CLI →
  callCliAdvisor).
- tests/debate-cli.test.ts — 9 new cases mirroring council-cli.test.ts.
- SPEC.md + README.md — note CLI support in the debate section.

Tests: 268 → 277 (all green). Typecheck clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant