Skip to content

feat: override a CLI profile's binary per project and per run #395

Description

@albi-bon

Describe your idea

Let a project override the executable a CLI profile spawns, without duplicating the whole profile:

  1. A binary field in [adapter] (and [adapter.dev|review|triage]) in policy.toml.
  2. Per-run flags on run/sweep: --cli <profile> and --cli-binary <executable>.

Why is this needed?

Users who hold more than one subscription/profile for the same coding CLI invoke it under different names — e.g. a work account launched as cw and a personal one as claude, or two aliases used to split usage limits. Today bmad-loop always spawns CLIProfile.binary, which is fixed per profile.

The overlay in .bmad-loop/profiles/*.toml can express this, but only by copying the entire packaged profile to change one line. That copy then silently freezes everything else: bypass_args, [env] (CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN, CLAUDE_CODE_DISABLE_BACKGROUND_TASKS), seed_files, and env_fault_patterns stop tracking upstream. A user who forked claude.toml before #194 landed would never have received the transport-fault patterns. The cost of changing one field is permanent drift on eight others.

Choosing the account per run is also not expressible at all: policy.toml is project-scoped state, so switching accounts means editing a file rather than passing a flag — awkward when alternating profiles on the same repo.

How should it work?

The seam already exists and is simply unwired: both GenericAdapter.__init__ (adapters/generic.py:417) and OpencodeHttpAdapter.__init__ (adapters/opencode_http.py:389) already accept binary: str | None = None and do self.binary = binary or profile.binary. Nothing passes it — only probe-adapter --binary uses a parallel path.

Proposed:

Policy fieldAdapterPolicy.binary: str = "" and StageAdapterPolicy.binary: str | None = None, surfaced through ResolvedAdapter. ""/None = use the profile's binary, so the default is unchanged. Inheritance follows the existing model/extra_args rule in AdapterPolicy.resolved: client-specific, inherited from the base only when same_client, so a stage that switches profile doesn't carry the other CLI's executable. runsetup.make_adapters passes cfg.binary or None into the ctor that already takes it.

Per-run flags--cli / --cli-binary on run and sweep, applied as a dataclasses.replace on the policy before the snapshot is stamped into RunState, so resume, the --json adapters projection (documents.py:255) and per-task adapter stamping all inherit the override with no extra threading.

Also neededcmd_validate's adapter.binary check (cli.py:294) currently probes p.binary per profile; it has to probe the effective binary, otherwise validate reports "claude found" for a run that will spawn cc.

Constraint worth documenting rather than detecting: an override assumes the same hook dialect, hooks.config_path and transcript layout as the profile it overrides. It is for an alias/wrapper of the same CLI, not a way to point claude at a different tool.

Nothing here touches the control loop, adds an LLM call, or changes a completion path.

PR

I'd like to contribute this, split into two PRs so each is one logical change:

  1. the [adapter] binary policy field + plumbing + the cmd_validate fix
  2. the --cli / --cli-binary per-run flags, based on (1)

Happy to adjust the shape (or drop half of it) before I open anything — flagging first per CONTRIBUTING.md.

Additional context

Related surfaces I expect to touch: policy.py (dataclasses, _parse_policy, _stage_adapter, adapter_policy_from_snapshot, _stage_from_snapshot, the embedded policy template), data/settings/core.toml (the sync test enforces field parity), runsetup.py, cli.py, plus docs/FEATURES.md and the changelog.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions