Skip to content

feat(cli): add --cli and --cli-binary to run and sweep - #398

Draft
albi-bon wants to merge 5 commits into
bmad-code-org:mainfrom
albi-bon:feat/cli-binary-flags
Draft

feat(cli): add --cli and --cli-binary to run and sweep#398
albi-bon wants to merge 5 commits into
bmad-code-org:mainfrom
albi-bon:feat/cli-binary-flags

Conversation

@albi-bon

@albi-bon albi-bon commented Jul 30, 2026

Copy link
Copy Markdown

Stacked on #397, which must merge first; this branch needs a rebase once it does. #399 is a sibling — it needs nothing from this PR, and vice versa. Review the feat(cli): add --cli and --cli-binary… commits for this change alone. Base reads main only because a cross-repo PR cannot target a fork branch.

What

run and sweep take --cli <profile> and --cli-binary <executable>: the per-run form of [adapter] name and [adapter] binary.

bmad-loop run --cli-binary cc                        # this run on a second account
bmad-loop run --cli codex                            # this run on another client
bmad-loop run --cli codex --cli-binary /opt/wk/codex # both
bmad-loop sweep --cli-binary cc

Why

Fixes #395 (part 2 of 3).

#397 makes the account choice expressible, but only as project-scoped state: switching means editing policy.toml. That is awkward when alternating work and personal accounts on the same repo, and worse when one account hits its usage limit mid-sprint and you want the next run elsewhere.

How

  • One shared _add_cli_override_flags on both subparsers, so run and sweep cannot drift. Naming follows probe-adapter's existing vocabulary — cli is the profile, binary the executable — so one word means one thing across the CLI surface.
  • _apply_cli_overrides folds the flags into the policy right after load, before the run composes. That is the whole trick: the policy is what gets stamped into RunState, so resume, the --json adapter projection, per-task adapter stamping and the --dry-run render all follow with no further threading.
  • A flag is a whole-run choice, so it beats the per-stage [adapter.<stage>] tables as well as the base. Clearing is surgical and mirrors AdapterPolicy.resolved's own client-switch rule: a stage whose effective client the flag changes drops the model/extra_args/binary it had chosen for the old client, and keeps its timing knobs (those mean "fall back to the profile default" and aren't client-specific). A stage already running the forced client keeps everything.
  • --cli-binary stops at a stage pinned to another client, and says so on stderr. An executable belongs to one CLI — the profile decides the prompt dialect, hook contract and bypass flags, so handing a cc alias of claude to a codex-pinned stage would give claude --dangerously-bypass-approvals-and-sandbox and kill the session at startup. The note names the stage, the client it runs, and the way out (--cli forces one client for the whole run). Silent when the flag reaches every stage, which is the homogeneous-config default.
  • Flag values get the same normalization policy.toml does: trimmed, and a present-but-blank value refused rather than read as unset.
  • An unknown --cli raises before anything spawns, with the same unknown CLI profile … (available: …) message make_adapters gives for a bad policy value — needed because the dry-run render resolves the profile too, so it would otherwise surface as a traceback.

Baseline with codex configured for review and custom binary configured for dev
Image

Custom binary specified per-run, warning that it does not get applied to the custom review agent
image

Custom goes back to harness-default when --cli arg is used
image

Two calls I'd flag for you

  • resume takes no flags. It rebuilds from the stamped snapshot, so a resumed run already inherits whatever it was launched with. Adding flags there would mean deciding whether a resume may change client mid-run, which seems like its own conversation.
  • A bad --cli exits FAILURE (1), not USAGE (2), to match what the identical unknown-profile error already does when it comes from policy.toml. Say the word if you'd rather have 2 and I'll split the paths.

Testing

uv run pytest -q — 3709 passed, with the same two pre-existing macOS-only failures noted in #397 (invalid-UTF-8 filenames APFS rejects), which reproduce on a clean checkout. uv run pyright reports the same 2 pre-existing os.setxattr errors before and after. ruff check clean; formatting verified with the black and prettier versions pinned in .trunk/trunk.yaml.

Coverage in tests/test_cli.py: the no-flag identity case; --cli-binary reaching every stage; the client-specific carve-out (a cc alias not leaking into a codex-pinned stage) and the note that reports it; silence when the flag reaches everything and when --cli collapses the config first; --cli dropping stale stage settings while keeping timing knobs; the blank-value and unknown-profile refusals; plus end-to-end run --dry-run and sweep assertions. Per the AGENTS.md ablation rule I removed the _apply_cli_overrides call from both call sites, and separately the warning call, and confirmed the corresponding tests FAIL first.

Also exercised by hand against a scratch BMAD project — the screenshots above are that session.

albi-bon added 2 commits July 30, 2026 18:12
…g#395)

A profile's binary was fixed, so running the same CLI under a second name
— a work account launched as `cc` beside a personal `claude`, or two
aliases splitting usage limits — meant copying the whole packaged profile
into .bmad-loop/profiles/ to change one field. That copy then freezes
bypass_args, [env], seed_files and env_fault_patterns against upstream
forever: the cost of one field is permanent drift on eight others.

Add `binary` to [adapter] and [adapter.<stage>]. Empty (the default) keeps
the profile's own binary, so existing projects are untouched. Both adapter
families already accepted a `binary` argument and resolved it as
`binary or profile.binary`; policy is what was never plumbed into it, so
make_adapters passes the resolved value and the seam does the rest.

Inheritance follows the model/extra_args rule: client-specific, inherited
from the base only when the stage runs the same client, so a `cc` alias of
claude can never become argv[0] for a stage that switched to codex.

Two read paths had to follow the effective binary rather than the
profile's: validate's PATH probe (it would report "claude found" for a run
that spawns a typo'd `cc`) and the --dry-run invocation render.

The override is for an alias or wrapper of the same CLI — the profile
still supplies the hook dialect, config path and transcript layout.
…#395)

Picking the client or the account for a single run meant editing
policy.toml, which is project-scoped state — awkward when alternating
work and personal accounts on one repo, or when one account hits its
usage limit mid-sprint.

Both flags fold into the policy before the run composes, not into the
engine, so the RunState snapshot records what actually ran: resume, the
--json adapter projection, per-task stamping and --dry-run all follow
with no further wiring.

A flag is a whole-run choice, so it beats the per-stage tables rather
than only the base. Clearing mirrors AdapterPolicy.resolved's own
client-switch rule: a stage whose effective client the flag changes drops
the model/extra_args/binary it chose for the old one and keeps its timing
knobs; a stage already on the forced client is left alone.

An unknown --cli fails before anything spawns — the dry-run render
resolves the profile too, so it would otherwise surface as a traceback.

Naming follows probe-adapter's vocabulary: `cli` is the profile, `binary`
the executable.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ca378017-fb70-4bfe-a412-db0b1baf3a92

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

albi-bon added 2 commits July 30, 2026 18:38
…org#395)

The flag path skipped the normalization the TOML path gets, so
`--cli-binary "   "` silently read as unset and `--cli-binary "  cc  "`
became a whitespace-padded argv[0] that dies at spawn with an
unrecognizable error. Route both flags through the same trim-and-refuse
rule policy.toml already applies.
…ad-code-org#395)

The base table reset model and extra_args on a --cli client switch but
kept binary, so a `cc` alias of claude survived `--cli codex` and the run
previewed as `cc-work "Use the $bmad-dev-auto skill…" --dangerously-bypass-
approvals-and-sandbox` — claude's executable driven with codex's prompt
template and bypass flags. binary is client-specific like the other two;
give the base the same reset the stage loop already performed. An explicit
--cli-binary still wins, and forcing the client already in use is not a
switch, so a configured alias survives that.
A binary belongs to one client: the profile decides the prompt dialect,
hook contract and bypass flags, so spawning a `cc` alias of claude for a
codex-pinned stage hands claude `--dangerously-bypass-approvals-and-
sandbox` and kills the session at startup. The flag therefore stops at a
stage running another client.

Doing that silently was the mistake. An operator who passes a whole-run
flag had to read a --dry-run to notice it applied to only some stages.
Name the stage, the client it runs, and the way out (--cli forces one
client for the whole run). Nothing changes when the flag reaches
everything, which is the homogeneous-config default.
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.

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

1 participant