From 2be6732444dff81d22c5e7ebaa1c1a54ba5a4203 Mon Sep 17 00:00:00 2001 From: Tien Le Date: Tue, 11 Aug 2026 21:37:17 +0000 Subject: [PATCH 1/2] launch: remove the `ucode --dry-run` flag `ucode --dry-run` launched the managed agent from the last saved managed-state.json without fetching, and wrote no config files. In practice it was a confusing third meaning of --dry-run (distinct from `ucode configure --dry-run` and `ucode setup --dry-run`) and let a launch run off a stale local copy of the managed config. Drop it: bare `ucode` always refreshes the managed config from the workspace before launching, which is the only source of truth. `ucode configure --dry-run` (preview config files) and `ucode setup --dry-run` (walk the authoring flow without writing) are unchanged. - Remove the flag and its `set_dry_run`/threading from the top-level callback and `_launch_managed_default` (which now always refreshes). - Drop the launch-path `is_dry_run()` guards that can no longer be true, and the now-unused `is_dry_run` import in cli.py. - Revert the #309 docs/next-steps that pointed at `ucode --dry-run` as the local try-out step (setup Next steps, README, managed_config/managed_wizard docstrings). - Remove the launch dry-run test; configure/setup dry-run tests stay. Co-authored-by: Isaac --- README.md | 10 +++------ src/ucode/cli.py | 41 +++++++------------------------------ src/ucode/managed_config.py | 12 +++++------ src/ucode/managed_wizard.py | 12 +++-------- tests/test_cli.py | 19 ----------------- 5 files changed, 19 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index f03cad38..4bde8566 100644 --- a/README.md +++ b/README.md @@ -186,10 +186,9 @@ skipped. It then offers tracing, managed MCP servers, skills, and a spend-based switches the default agent and model as the workspace burns through a budget. The result is written to `~/.ucode/managed-state.json` — the one local managed-config file — which -`ucode apply` publishes to the workspace. Because a launch reads that same file, `ucode --dry-run` -tries the authored config on this machine before it is published, without fetching or overwriting -it. Your own agent configs are left alone, with one exception: answering yes to tracing, MCP -servers, or skills runs the matching `ucode configure` step, which does configure this machine. +`ucode apply` publishes to the workspace. Your own agent configs are left alone, with one exception: +answering yes to tracing, MCP servers, or skills runs the matching `ucode configure` step, which +does configure this machine. ```bash # Review the manifest and the exact payload `ucode apply` would publish. @@ -198,9 +197,6 @@ ucode setup show # Walk the flow without writing anything. ucode setup --dry-run -# Try the authored config locally before publishing (no fetch, no overwrite). -ucode --dry-run - # Skip the prompts and load a hand-written config instead (validated before saving). ucode setup --from-file ./managed-config.json ``` diff --git a/src/ucode/cli.py b/src/ucode/cli.py index 3c60b3b6..94bdf6b2 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -33,7 +33,7 @@ ) from ucode.agents.codex import revert_legacy_shared_config from ucode.agents.pi import PI_SETTINGS_BACKUP_PATH, PI_SETTINGS_PATH -from ucode.config_io import is_dry_run, restore_file, set_dry_run +from ucode.config_io import restore_file, set_dry_run from ucode.databricks import ( apply_pat_environment, build_shared_base_urls, @@ -1378,9 +1378,7 @@ def _fetch_budget_recommendation( Enforcement is server-side, so a failed read only costs the recommendation: the config's own ``default_model`` still applies and the launch proceeds. """ - # --dry-run resolves the agent from the last saved config alone, so it must not reach the - # control plane — mirroring the managed-config read, which is likewise skipped under --dry-run. - if managed is None or skip_preflight or is_dry_run(): + if managed is None or skip_preflight: return None reason: str | None = None recommendation = None @@ -1699,8 +1697,8 @@ def _launch_tool( # Register the managed config's MCP servers so they reach the agent's `/mcp` list. Nothing # else on this path does it — the config only lists them — so without this a # workspace-published server never shows up. Skipped under --skip-preflight (deliberately - # unmanaged) and --dry-run (writes nothing). - if managed is not None and not skip_preflight and not is_dry_run(): + # unmanaged). + if managed is not None and not skip_preflight: _register_managed_mcp_servers(managed, tool, state) print_success(f"Starting {TOOL_SPECS[tool]['display']}") launch_agent(tool, state, ctx.args) @@ -1751,14 +1749,6 @@ def default( is_eager=True, ), ] = False, - dry_run: Annotated[ - bool, - typer.Option( - "--dry-run", - help="Print config files without writing them. Uses the last saved managed " - "config instead of fetching a fresh one.", - ), - ] = False, skip_preflight: SkipPreflightOption = False, workspace: WorkspaceOption = None, ) -> None: @@ -1768,11 +1758,8 @@ def default( """ if ctx.invoked_subcommand is not None: return - set_dry_run(dry_run) try: - _launch_managed_default( - ctx, dry_run=dry_run, skip_preflight=skip_preflight, workspace=workspace - ) + _launch_managed_default(ctx, skip_preflight=skip_preflight, workspace=workspace) except typer.Exit: # `typer.Exit` subclasses RuntimeError, so it has to be re-raised ahead of the handler # below. Otherwise a launch that already reported its own error is followed by @@ -1786,7 +1773,6 @@ def default( def _launch_managed_default( ctx: typer.Context, *, - dry_run: bool, skip_preflight: bool, workspace: str | None, ) -> None: @@ -1808,22 +1794,9 @@ def _launch_managed_default( "--skip-preflight launches with your own settings, so `ucode` has no managed config " "to pick an agent from. Run `ucode --skip-preflight` instead." ) - # --dry-run avoids the fetch but still applies the last saved config. - if dry_run: - managed = load_managed_state(current) - else: - with spinner("Checking for a managed coding agent config..."): - managed = refresh_managed_config(state) + with spinner("Checking for a managed coding agent config..."): + managed = refresh_managed_config(state) if not managed: - # Only a read that actually reached the workspace can say it publishes no config. Under - # --dry-run nothing was fetched, so an empty cache means "not pulled yet" — reporting that - # as "no config" would tell an admin their own published config doesn't exist. - if dry_run: - print_warning( - "No managed coding agent config is saved locally yet, so there is nothing to " - "dry-run. Run `ucode` without --dry-run to pull your workspace's config first." - ) - return _print_no_managed_config_guidance(current, state.get("profile")) return # The budget tier can move the org to a cheaper agent, so it outranks the config's diff --git a/src/ucode/managed_config.py b/src/ucode/managed_config.py index 5e9bbcee..d7da5bdc 100644 --- a/src/ucode/managed_config.py +++ b/src/ucode/managed_config.py @@ -13,8 +13,8 @@ There is deliberately one file, not a separate authored ``managed-settings.json``: the workspace is the source of truth, so an authored draft and the pulled copy are the same shape and coexist in -``managed-state.json``. ``ucode --dry-run`` reads the local draft without fetching or overwriting it, -which is how an admin tries a config out between ``ucode setup`` and ``ucode apply``. +``managed-state.json``. ``ucode setup`` authors the draft; ``ucode apply`` publishes it; a launch +then pulls the published copy back into the same file. :func:`refresh_managed_config` is the launch path's entry point. It is called before model discovery, because the manifest decides whether that discovery is needed at all; the launch path then hands the @@ -379,10 +379,10 @@ def load_managed_state(workspace: str | None) -> dict | None: Returns the normalized config dict (the ``config`` field), only when the stored file is for the same workspace — so a stale file from another workspace is ignored rather than misapplied. - This is the single local managed config: ``ucode setup`` authors it here, ``ucode --dry-run`` - reads it to try the authored config locally, ``ucode apply`` publishes it, and a normal launch - refreshes it from the workspace. The admin-authored draft and the pulled copy share one file - because the workspace is the source of truth — to keep a draft, publish it with ``ucode apply``. + This is the single local managed config: ``ucode setup`` authors it here, ``ucode apply`` + publishes it, and a launch refreshes it from the workspace. The admin-authored draft and the + pulled copy share one file because the workspace is the source of truth — to keep a draft, + publish it with ``ucode apply``. """ if not workspace: return None diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index f5da862b..2c80990f 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -3,9 +3,8 @@ Workspace admins run this to build the ``CodingAgentConfig`` their developers will pull. It walks the admin through agents, per-agent models, tracing, MCP servers, skills, and a spend-routing budget policy, then writes the manifest to ``~/.ucode/managed-state.json`` (the one local managed-config -file, owned by :mod:`ucode.managed_config`). An admin can try it with ``ucode --dry-run`` and then -publish it to the workspace with ``ucode apply`` (a separate command, so the file can be reviewed -first). +file, owned by :mod:`ucode.managed_config`). Publishing it to the workspace is ``ucode apply`` (a +separate command, so an admin can review the file first). Serialization, validation, and the per-agent model catalogs live in :mod:`ucode.managed_setup`; this module is the interaction layer on top of them. Sub-flows an admin already knows — tracing, MCP, @@ -1001,10 +1000,6 @@ def setup_from_file(path: str) -> int: def _print_next_steps() -> None: console.print() print_heading("Next steps") - # The authored manifest is saved to the same local file a launch reads, so `ucode --dry-run` - # previews this machine's agents *as configured by the manifest* without fetching or overwriting - # it — a real local test of the config before it is published. - print_note("Try it locally: ucode --dry-run") print_note("Publish it to the workspace: ucode apply") @@ -1036,8 +1031,7 @@ def setup_command(from_file: str | None = None) -> int: if not _handle_existing_config(workspace, token): return 0 - # Discover the workspace's models and gateway URLs. This also logs in and persists local state, - # which is what lets the admin dry-run the config on their own machine afterwards. + # Discover the workspace's models and gateway URLs. This also logs in and persists local state. state = configure_shared_state(workspace, profile=profile, force_login=False) workspace = state.get("workspace") or workspace profile = state.get("profile") or profile diff --git a/tests/test_cli.py b/tests/test_cli.py index d0049373..f1c19cfc 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2557,25 +2557,6 @@ def test_non_admin_without_a_config_is_told_to_ask(self, monkeypatch): assert launched == [] assert "Ask a workspace admin" in result.output - def test_dry_run_uses_the_cache_and_does_not_fetch(self, monkeypatch): - monkeypatch.setenv("ENABLE_MANAGED_AGENT_CONFIG", "1") - monkeypatch.setattr("ucode.cli.install_databricks_cli", lambda *a, **k: None) - monkeypatch.setattr("ucode.cli.apply_pat_environment", lambda *a, **k: None) - monkeypatch.setattr("ucode.cli.load_state", lambda: {"workspace": "https://w"}) - monkeypatch.setattr( - "ucode.cli.refresh_managed_config", - lambda state: pytest.fail("--dry-run must not fetch"), - ) - monkeypatch.setattr("ucode.cli.load_managed_state", lambda ws: self.MANAGED) - launched: list[tuple] = [] - monkeypatch.setattr( - "ucode.cli._launch_tool", lambda tool, ctx, **kw: launched.append((tool, kw)) - ) - result = runner.invoke(app, ["--dry-run"]) - assert result.exit_code == 0, result.output - # The config bare `ucode` already read is handed down, so the launch path does not refetch. - assert launched[0][1]["managed"] == self.MANAGED - def test_skip_preflight_has_no_config_to_pick_an_agent_from(self, monkeypatch): # --skip-preflight is deliberately unmanaged, so bare `ucode` cannot resolve an agent. It # must say that rather than report "no config found", which would be wrong. From f45f69f1001c7797d55c10f0f13e0e6c4b5e5c2f Mon Sep 17 00:00:00 2001 From: Tien Le Date: Thu, 13 Aug 2026 17:10:38 +0000 Subject: [PATCH 2/2] setup: remove the `ucode setup --dry-run` flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following the launch `--dry-run` removal, drop `ucode setup --dry-run` too. It "walked the flow without writing any files", but the authoring flow's only writes are the local manifest (which `ucode setup show` / `--from-file` already cover) and a confirmed delete of an existing published config — so a dedicated dry-run added little over the existing review paths. - Remove the flag and its `set_dry_run` from the `setup` callback (`set_dry_run` stays for `ucode configure --dry-run`). - Drop the now-dead `is_dry_run()` branches in the setup flow (setup_command, setup_from_file, and the delete-confirmation path) and the unused import. - Update tests and the README. `save_managed_state` keeps respecting the global dry-run flag like the other config writers (still exercised by its unit test and `ucode configure --dry-run`). Co-authored-by: Isaac --- README.md | 3 --- src/ucode/cli.py | 5 ----- src/ucode/managed_wizard.py | 22 +++++----------------- tests/test_managed_wizard.py | 26 -------------------------- 4 files changed, 5 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 4bde8566..37490a2f 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,6 @@ does configure this machine. # Review the manifest and the exact payload `ucode apply` would publish. ucode setup show -# Walk the flow without writing anything. -ucode setup --dry-run - # Skip the prompts and load a hand-written config instead (validated before saving). ucode setup --from-file ./managed-config.json ``` diff --git a/src/ucode/cli.py b/src/ucode/cli.py index 94bdf6b2..36d4f474 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -2442,15 +2442,10 @@ def setup( "ucode's manifest shape) instead. Validated before it is saved.", ), ] = None, - dry_run: Annotated[ - bool, - typer.Option("--dry-run", help="Walk the flow without writing any files."), - ] = False, ) -> None: """Author the managed coding config for your workspace (workspace admins only).""" if ctx.invoked_subcommand is not None: return - set_dry_run(dry_run) # `typer.Exit` subclasses RuntimeError, so it must be raised outside the try — inside, the # `except RuntimeError` below would swallow it and report the exit code as an error message. try: diff --git a/src/ucode/managed_wizard.py b/src/ucode/managed_wizard.py index 2c80990f..11be377f 100644 --- a/src/ucode/managed_wizard.py +++ b/src/ucode/managed_wizard.py @@ -19,7 +19,6 @@ from typing import cast from ucode.agents import TOOL_SPECS, check_gateway_endpoint -from ucode.config_io import is_dry_run from ucode.databricks import ( ANTHROPIC_FAMILIES, all_users_can_use_schema, @@ -925,7 +924,7 @@ def _delete_existing_config(workspace: str, token: str, existing: dict) -> None: """Delete the workspace's published config after confirming. Raises RuntimeError on failure. Deleting leaves the workspace with no managed config, so every developer falls back to their own - settings on their next ucode run — confirm before doing it, and honor ``--dry-run``. + settings on their next ucode run — confirm before doing it. """ name = existing.get("name") if not isinstance(name, str): @@ -940,9 +939,6 @@ def _delete_existing_config(workspace: str, token: str, existing: dict) -> None: if not prompt_yes_no_default("Delete the existing managed config?", default=False): print_note("Nothing was deleted.") return - if is_dry_run(): - print_success("Dry run: the config was not deleted.") - return with spinner("Deleting the managed config..."): delete_reason = delete_coding_agent_config(workspace, token, name) if delete_reason is not None: @@ -987,13 +983,8 @@ def setup_from_file(path: str) -> int: save_managed_state(workspace, manifest) _render_summary(workspace, manifest) - if is_dry_run(): - print_success( - f"Dry run: {manifest_path.name} was not written to ~/.ucode/managed-state.json." - ) - else: - print_success(f"Saved to {manifest_path.name} -> ~/.ucode/managed-state.json") - _print_next_steps() + print_success(f"Saved to {manifest_path.name} -> ~/.ucode/managed-state.json") + _print_next_steps() return 0 @@ -1132,11 +1123,8 @@ def setup_command(from_file: str | None = None) -> int: save_managed_state(workspace, manifest) _render_summary(workspace, manifest) console.print() - if is_dry_run(): - print_success("Dry run: nothing was written to ~/.ucode/managed-state.json.") - else: - print_success("Saved to ~/.ucode/managed-state.json") - _print_next_steps() + print_success("Saved to ~/.ucode/managed-state.json") + _print_next_steps() return 0 diff --git a/tests/test_managed_wizard.py b/tests/test_managed_wizard.py index cc4b8e50..313daec6 100644 --- a/tests/test_managed_wizard.py +++ b/tests/test_managed_wizard.py @@ -279,7 +279,6 @@ def test_choosing_delete_stops_and_deletes(self): ), patch.object(wizard, "prompt_for_selection", return_value="delete"), patch.object(wizard, "prompt_yes_no_default", return_value=True), - patch.object(wizard, "is_dry_run", return_value=False), patch.object(wizard, "delete_coding_agent_config", return_value=None) as delete, ): assert wizard._handle_existing_config(WORKSPACE, "token") is False @@ -300,21 +299,6 @@ def test_delete_declined_leaves_config_intact(self): assert wizard._handle_existing_config(WORKSPACE, "token") is False assert not delete.called - def test_delete_honors_dry_run(self): - with ( - patch.object( - wizard, - "get_managed_config", - return_value=({"name": "cfg/1", "enabled_agents": {}}, None), - ), - patch.object(wizard, "prompt_for_selection", return_value="delete"), - patch.object(wizard, "prompt_yes_no_default", return_value=True), - patch.object(wizard, "is_dry_run", return_value=True), - patch.object(wizard, "delete_coding_agent_config") as delete, - ): - assert wizard._handle_existing_config(WORKSPACE, "token") is False - assert not delete.called - def test_delete_failure_raises(self): with ( patch.object( @@ -324,7 +308,6 @@ def test_delete_failure_raises(self): ), patch.object(wizard, "prompt_for_selection", return_value="delete"), patch.object(wizard, "prompt_yes_no_default", return_value=True), - patch.object(wizard, "is_dry_run", return_value=False), patch.object(wizard, "delete_coding_agent_config", return_value="HTTP 500"), pytest.raises(RuntimeError, match="Could not delete"), ): @@ -2103,15 +2086,6 @@ def test_from_file_is_forwarded(self): runner.invoke(app, ["setup", "--from-file", "/tmp/x.json"]) assert setup.call_args.kwargs["from_file"] == "/tmp/x.json" - def test_dry_run_sets_the_flag(self): - with ( - patch("ucode.cli.install_databricks_cli"), - patch("ucode.cli.setup_command", return_value=0), - patch("ucode.cli.set_dry_run") as set_flag, - ): - runner.invoke(app, ["setup", "--dry-run"]) - set_flag.assert_called_once_with(True) - def test_show_exits_zero(self): with patch("ucode.cli.show_command", return_value=0): result = runner.invoke(app, ["setup", "show"])