diff --git a/src/ucode/cli.py b/src/ucode/cli.py index c5294407..9af1c2dd 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -1812,6 +1812,26 @@ def _smart_routing_v2_flag(enabled: bool) -> Iterator[None]: os.environ[smart_routing_v2.ENV_VAR] = previous +@contextmanager +def _disable_smart_routing_for_subcommand(tool: str, ctx: Any) -> Iterator[None]: + """Keep native agent subcommands out of every smart-routing path. + + The environment flag is also consulted during bootstrap/version checks, + before the final launch options are built. Native positional subcommands + must therefore suppress the flag for the whole ucode launch flow. An + explicit prompt after `--` remains eligible for routing. + """ + if _smart_routing_launch_shape(tool, ctx.args, _has_explicit_prompt(ctx)): + yield + return + previous = os.environ.pop(smart_routing_v2.ENV_VAR, None) + try: + yield + finally: + if previous is not None: + os.environ[smart_routing_v2.ENV_VAR] = previous + + def _migrate_legacy_smart_routing(state: dict) -> dict: """Remove the former persisted opt-in and its permanent routing hooks.""" if smart_routing_v2.LEGACY_STATE_KEY not in state: @@ -1990,6 +2010,11 @@ def _should_launch_smart_routing( ) -> bool: if model is not None or has_explicit_model_arg(tool_args): return False + return _smart_routing_launch_shape(tool, tool_args, explicit_prompt) + + +def _smart_routing_launch_shape(tool: str, tool_args: list[str], explicit_prompt: bool) -> bool: + """Whether the forwarded arguments represent an interactive launch.""" if not tool_args or explicit_prompt: return True return tool == "claude" and tool_args[0].startswith("-") @@ -2558,15 +2583,16 @@ def codex_cmd( print_success("Codex smart routing disabled; ug routing hooks removed") return with _smart_routing_v2_flag(enable_smart_routing_flag): - _launch_tool( - "codex", - ctx, - provider=provider, - refresh=refresh, - skip_preflight=skip_preflight, - workspace_url=workspace, - custom_oauth=custom_oauth, - ) + with _disable_smart_routing_for_subcommand("codex", ctx): + _launch_tool( + "codex", + ctx, + provider=provider, + refresh=refresh, + skip_preflight=skip_preflight, + workspace_url=workspace, + custom_oauth=custom_oauth, + ) @app.command( @@ -2659,16 +2685,17 @@ def claude_cmd( if enable_model_discovery: os.environ[claude_agent.GATEWAY_MODEL_DISCOVERY_ENV_VAR] = "1" with _smart_routing_v2_flag(enable_smart_routing_flag): - _launch_tool( - "claude", - ctx, - provider=provider, - model=model, - refresh=refresh, - skip_preflight=skip_preflight, - workspace_url=workspace, - custom_oauth=custom_oauth, - ) + with _disable_smart_routing_for_subcommand("claude", ctx): + _launch_tool( + "claude", + ctx, + provider=provider, + model=model, + refresh=refresh, + skip_preflight=skip_preflight, + workspace_url=workspace, + custom_oauth=custom_oauth, + ) @app.command("gemini", context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) diff --git a/tests/test_cli.py b/tests/test_cli.py index 32e81f71..a0d526d8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -459,6 +459,25 @@ def test_codex_enable_smart_routing_is_consumed_by_ucode(self): assert "ENABLE_SMART_ROUTING_V2" not in os.environ assert mock_launch.call_args.args[1].args == [] + @pytest.mark.parametrize("tool, subcommand", [("codex", "app"), ("claude", "update")]) + def test_native_subcommand_suppresses_inherited_smart_routing( + self, monkeypatch, tool, subcommand + ): + monkeypatch.setenv("ENABLE_SMART_ROUTING_V2", "1") + observed = [] + + with patch( + "ucode.cli._launch_tool", + side_effect=lambda *_args, **_kwargs: observed.append( + os.environ.get("ENABLE_SMART_ROUTING_V2") + ), + ): + result = runner.invoke(app, [tool, subcommand]) + + assert result.exit_code == 0, result.output + assert observed == [None] + assert os.environ["ENABLE_SMART_ROUTING_V2"] == "1" + def test_claude_enable_smart_routing_forwards_positional_prompt_to_v2(self): captured = []