diff --git a/plugins/nemo-eval-author/README.md b/plugins/nemo-eval-author/README.md index 7f22176cac..9414a77ff0 100644 --- a/plugins/nemo-eval-author/README.md +++ b/plugins/nemo-eval-author/README.md @@ -63,4 +63,9 @@ Experimentalist import, both tagged `TODO(eval-author-standalone)`: ahead of any Experimentalist agent, because those agents read the environment when their class body executes. -A `nemo eval-author` CLI that auto-loads this `.env` is not wired yet. +A `nemo agents eval-author` CLI is registered under `nemo.cli.agents` and +mounted by the agents plugin. Verb scaffolding is in place +(`discover`, `audit`, `propose`, `run`, `doctor`); bodies are still +placeholders until ASE-673–678 land. The CLI does not auto-load this `.env` +yet — set credentials in the environment (or rely on the transitional +`EXPERIMENTALIST_*` fallback) before invoking it. diff --git a/plugins/nemo-eval-author/pyproject.toml b/plugins/nemo-eval-author/pyproject.toml index 65dedcd6c3..33212fcb09 100644 --- a/plugins/nemo-eval-author/pyproject.toml +++ b/plugins/nemo-eval-author/pyproject.toml @@ -14,9 +14,6 @@ dependencies = [ "tomlkit>=0.13.3", ] -[project.entry-points."nemo.cli"] -eval-author = "nemo_eval_author_plugin.cli:EvalAuthorCLI" - [project.entry-points."nemo.cli.agents"] eval-author = "nemo_eval_author_plugin.cli:EvalAuthorCLI" diff --git a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/cli.py b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/cli.py index 7a4d4401b0..8e9b2d3f79 100644 --- a/plugins/nemo-eval-author/src/nemo_eval_author_plugin/cli.py +++ b/plugins/nemo-eval-author/src/nemo_eval_author_plugin/cli.py @@ -3,9 +3,8 @@ """Eval Author plugin CLI — ``nemo agents eval-author ...`` subcommands. -The same class is registered under both ``nemo.cli.agents`` and ``nemo.cli``, so every -verb is reachable as ``nemo agents eval-author `` (canonical) and as ``nemo -eval-author `` (retained for backward compatibility). +Registered under ``nemo.cli.agents`` and mounted by ``AgentsCLI`` as +``nemo agents eval-author ``. Scaffolding. Every verb is registered under its final name so the command tree is discoverable and the child tickets have a landing spot, and each body exits non-zero @@ -28,8 +27,7 @@ def _not_implemented(ctx: typer.Context, ticket: str) -> NoReturn: """Fail loudly, so a placeholder verb can never be mistaken for a successful run. - The message quotes ``ctx.command_path`` rather than a hardcoded path, so it names - whichever of the two mount points the caller actually used. + The message quotes ``ctx.command_path`` rather than a hardcoded path. """ typer.echo(f"`{ctx.command_path}` is not implemented yet ({ticket}).", err=True) raise typer.Exit(code=1) diff --git a/plugins/nemo-eval-author/tests/test_cli.py b/plugins/nemo-eval-author/tests/test_cli.py index 7f15ddff5a..ae6e71218b 100644 --- a/plugins/nemo-eval-author/tests/test_cli.py +++ b/plugins/nemo-eval-author/tests/test_cli.py @@ -1,16 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Scaffolding tests: the command tree exists, and every verb still refuses to run. - -The entry-point cases cover the ``pyproject.toml`` wiring that nothing else exercises. A -typo in the key or the import path does not fail an import; it just makes ``nemo agents -eval-author`` quietly missing from the CLI, which no unit test of this module would catch. -The class is registered twice — canonically under ``nemo.cli.agents`` and, for backward -compatibility, under ``nemo.cli`` — so both groups are asserted. -""" - -from importlib.metadata import EntryPoint, entry_points +"""Scaffolding tests: the command tree exists, and every verb still refuses to run.""" import pytest import typer @@ -34,12 +25,6 @@ def app() -> typer.Typer: return cli.EvalAuthorCLI().get_cli() -def _eval_author_entry_point(group: str = "nemo.cli") -> EntryPoint: - matches = [entry for entry in entry_points(group=group) if entry.name == "eval-author"] - assert matches, f"no {group} entry point named 'eval-author'; reinstall the plugin with uv sync" - return matches[0] - - def test_help_lists_every_verb(app: typer.Typer) -> None: result = runner.invoke(app, ["--help"]) @@ -56,42 +41,19 @@ def test_verb_refuses_to_run_and_names_its_ticket(app: typer.Typer, command: str assert ticket in result.output -@pytest.mark.parametrize("group", ["nemo.cli.agents", "nemo.cli"]) -def test_entry_point_key_matches_the_cli_name(group: str) -> None: - """Discovery rejects a plugin whose entry-point key differs from its ``name``.""" - assert _eval_author_entry_point(group).value == "nemo_eval_author_plugin.cli:EvalAuthorCLI" - assert cli.EvalAuthorCLI.name == "eval-author" - - -@pytest.mark.parametrize("group", ["nemo.cli.agents", "nemo.cli"]) -def test_entry_point_loads_the_cli_class(group: str) -> None: - assert _eval_author_entry_point(group).load() is cli.EvalAuthorCLI - +def test_not_implemented_quotes_the_invoked_command_path() -> None: + """Placeholder messages use ``ctx.command_path``, not a hardcoded CLI string.""" + app = typer.Typer() -def _mounted_under_agents() -> typer.Typer: - """The mount `AgentsCLI` performs, without importing the agents plugin.""" - agents = typer.Typer() - agents.add_typer(cli.EvalAuthorCLI().get_cli(), name="eval-author") - root = typer.Typer() - root.add_typer(agents, name="agents") - return root + @app.callback() + def _root() -> None: + """Force subcommand dispatch.""" + @app.command("probe") + def probe(ctx: typer.Context) -> None: + cli._not_implemented(ctx, "ASE-000") -@pytest.mark.parametrize(("command", "ticket"), _PLACEHOLDER_VERBS) -def test_verb_is_reachable_under_agents_and_names_that_path(command: str, ticket: str) -> None: - """The placeholder message must quote the path the caller typed, not a hardcoded one.""" - result = runner.invoke(_mounted_under_agents(), ["agents", "eval-author", command], prog_name="nemo") - - assert result.exit_code == 1, result.output - assert f"`nemo agents eval-author {command}` is not implemented yet ({ticket})." in result.output - - -@pytest.mark.parametrize(("command", "ticket"), _PLACEHOLDER_VERBS) -def test_legacy_top_level_path_still_works(command: str, ticket: str) -> None: - root = typer.Typer() - root.add_typer(cli.EvalAuthorCLI().get_cli(), name="eval-author") - - result = runner.invoke(root, ["eval-author", command], prog_name="nemo") + result = runner.invoke(app, ["probe"], prog_name="nemo") assert result.exit_code == 1, result.output - assert f"`nemo eval-author {command}` is not implemented yet ({ticket})." in result.output + assert "`nemo probe` is not implemented yet (ASE-000)." in result.output diff --git a/plugins/nemo-experimentalist/AGENTS.md b/plugins/nemo-experimentalist/AGENTS.md index 3b70c51f4b..c0df4af3f6 100644 --- a/plugins/nemo-experimentalist/AGENTS.md +++ b/plugins/nemo-experimentalist/AGENTS.md @@ -15,18 +15,15 @@ Inherited from the NeMo Platform monorepo that now hosts this plugin: ### 2026-07-31: Command group nested under `nemo agents` -The canonical path is `nemo agents experimentalist `. `ExperimentalistCLI` is +The only path is `nemo agents experimentalist `. `ExperimentalistCLI` is registered under the `nemo.cli.agents` entry-point group, which the `nemo-agents` -plugin's `AgentsCLI` discovers and mounts. +plugin's `AgentsCLI` discovers and mounts. There is no top-level +`nemo experimentalist` alias. -The `nemo.cli` registration stays, so `nemo experimentalist ` keeps working. Both -groups point at the same class, so a new verb is written once and appears under both — -do not add a second implementation for the legacy path. Docs, help text, and error -messages should name the `nemo agents` form; prefer `ctx.command_path` over a hardcoded -path when a message quotes the command back to the user. - -The analyst and Eval Author moved in the same change: `nemo agents analyst run` (was -`nemo insights analyze`) and `nemo agents eval-author `. +Analyst and Eval Author follow the same rule: `nemo agents analyst run` (was +`nemo insights analyze`) and `nemo agents eval-author `. Prefer +`ctx.command_path` over a hardcoded path when a message quotes the command back +to the user. ### 2026-07-28: Eval Author extracted to its own plugin, heading for standalone @@ -71,10 +68,11 @@ breaking rename with no compatibility aliases: - distribution `nemo-optimizer-plugin` → `nemo-experimentalist-plugin`, source path `src/nemo_optimizer_plugin` → `src/nemo_experimentalist_plugin` -- `OptimizerCLI` → `ExperimentalistCLI`, and both the `nemo.cli` and +- `OptimizerCLI` → `ExperimentalistCLI`, and the `nemo.cli.agents` and `nemo.skills` entry-point keys are now `experimentalist`, so the command is - `nemo experimentalist ...` -- the `experiment` verb is now `run`: `nemo experimentalist run` + `nemo agents experimentalist ...` (historically also briefly exposed as a + top-level `nemo experimentalist` alias; that alias is gone) +- the `experiment` verb is now `run`: `nemo agents experimentalist run` - `OPTIMIZER_API_BASE`, `OPTIMIZER_API_KEY`, `OPTIMIZER_{SMART,MID,FAST}_MODEL_NAME`, `OPTIMIZER_MODEL`, `NEMO_OPTIMIZER_E2E`, and `NEMO_OPTIMIZER_RUNTIME_CACHE` are now `EXPERIMENTALIST_*` / `NEMO_EXPERIMENTALIST_*` diff --git a/plugins/nemo-experimentalist/README.md b/plugins/nemo-experimentalist/README.md index 4ccd8c7f72..e38d8a8bb3 100644 --- a/plugins/nemo-experimentalist/README.md +++ b/plugins/nemo-experimentalist/README.md @@ -36,12 +36,12 @@ commit, currently one past `v0.0.6` that carries an MCP transport-timeout fix. The supported handoff is: ```text -nemo insights analyze → .nemo-optimizer/insights.yaml or Platform Insight ID - → nemo experimentalist doctor - → nemo experimentalist run +nemo agents analyst run → .nemo-optimizer/insights.yaml or Platform Insight ID + → nemo agents experimentalist doctor + → nemo agents experimentalist run ``` -Run `nemo insights analyze` using the Platform Insights plugin and its +Run `nemo agents analyst run` using the Platform Insights plugin and its documented trace, workspace, and output options. The producer may write the local profile default, `.nemo-optimizer/insights.yaml`, or persist an Insight on Platform and report its ID. The Experimentalist does not analyze traces, @@ -51,12 +51,12 @@ From an agent directory with an `optimizer.yaml` profile, validate the effective inputs: ```bash -$NEMO experimentalist doctor +$NEMO agents experimentalist doctor ``` ## Run the Experimentalist locally -`nemo experimentalist run` runs the local Experimentalist loop. It evaluates +`nemo agents experimentalist run` runs the local Experimentalist loop. It evaluates a baseline agent on Harbor-compatible train and validation datasets, proposes candidate mutations, and records its artifacts under the selected experiment directory. @@ -76,7 +76,7 @@ A single local Insight in `.nemo-optimizer/insights.yaml` is selected by default: ```bash -$NEMO experimentalist run +$NEMO agents experimentalist run ``` Use `--insight` to name another local file. Local files can contain one @@ -84,7 +84,7 @@ Insight object or an `insights` list. A list with multiple entries requires `--insight-id`, which accepts an exact ID, exact title, or zero-based index: ```bash -$NEMO experimentalist run \ +$NEMO agents experimentalist run \ --insight path/to/insights.yaml \ --insight-id 0 ``` @@ -92,7 +92,7 @@ $NEMO experimentalist run \ For an Insight persisted by Platform, pass its ID with its Platform location: ```bash -$NEMO experimentalist run \ +$NEMO agents experimentalist run \ --insight \ --workspace \ --base-url https:// @@ -108,7 +108,7 @@ Use `--no-insight` to bypass both an explicit Insight and the profile-local default. Supply a baseline agent when the profile does not provide one: ```bash -$NEMO experimentalist run \ +$NEMO agents experimentalist run \ --no-insight \ --agent path/to/agent \ --train-dataset path/to/train \ diff --git a/plugins/nemo-experimentalist/pyproject.toml b/plugins/nemo-experimentalist/pyproject.toml index 4b1eb0033e..842f9ff161 100644 --- a/plugins/nemo-experimentalist/pyproject.toml +++ b/plugins/nemo-experimentalist/pyproject.toml @@ -18,9 +18,6 @@ dependencies = [ "tomlkit>=0.13.3", ] -[project.entry-points."nemo.cli"] -experimentalist = "nemo_experimentalist_plugin.cli:ExperimentalistCLI" - [project.entry-points."nemo.cli.agents"] experimentalist = "nemo_experimentalist_plugin.cli:ExperimentalistCLI" diff --git a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py index 3e324d6a77..72e384faeb 100644 --- a/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py +++ b/plugins/nemo-experimentalist/src/nemo_experimentalist_plugin/cli.py @@ -3,9 +3,8 @@ """Experimentalist plugin CLI — ``nemo agents experimentalist ...`` subcommands. -The same class is registered under both ``nemo.cli.agents`` and ``nemo.cli``, so every -verb is reachable as ``nemo agents experimentalist `` (canonical) and as ``nemo -experimentalist `` (retained for backward compatibility). +Registered under ``nemo.cli.agents`` and mounted by ``AgentsCLI`` as +``nemo agents experimentalist ``. """ import asyncio diff --git a/plugins/nemo-insights/README.md b/plugins/nemo-insights/README.md index 5ca1ccf5fc..1a90e66cee 100644 --- a/plugins/nemo-insights/README.md +++ b/plugins/nemo-insights/README.md @@ -14,12 +14,12 @@ The plugin is installed by default through the root workspace's `enabled-plugins From an agent directory, Insights discovers `optimizer.yaml` in the current directory or its parents. Start by checking the profile and its environment, -then run analysis: +then run the analyst: ```bash cd -uv run nemo insights doctor -uv run nemo insights analyze +uv run nemo agents analyst doctor +uv run nemo agents analyst run ``` The profile contract consumed by Insights is deliberately small: @@ -48,7 +48,7 @@ With a discovered profile, analysis reads and writes the shared local output at `--insights-file-output` to use a different file explicitly. ```bash -uv run nemo insights analyze \ +uv run nemo agents analyst run \ --agent research-agent \ --workspace default \ --base-url http://localhost:8080 @@ -93,4 +93,4 @@ uv run ruff check plugins/nemo-insights ## Testbed The analyst-only testbed is in [`testbed/`](testbed/). It can replay pinned -Intake traces or run Tau2 benchmarks before invoking `nemo insights analyze`. +Intake traces or run Tau2 benchmarks before invoking `nemo agents analyst run`. diff --git a/plugins/nemo-insights/examples/research-agent/tests/test_analyst_e2e.py b/plugins/nemo-insights/examples/research-agent/tests/test_analyst_e2e.py index d0d21e285f..8519d31397 100644 --- a/plugins/nemo-insights/examples/research-agent/tests/test_analyst_e2e.py +++ b/plugins/nemo-insights/examples/research-agent/tests/test_analyst_e2e.py @@ -10,7 +10,7 @@ 3. Clear all spans for the test project. 4. Run the research agent on three questions (concurrently) so it logs traces to Intake. -5. Run the analyst agent (``nemo insights analyze``). +5. Run the analyst agent (``nemo agents analyst run``). 6. Assert the analyst created at least one Insight. Required setup (the test is **opt-in** because it costs real tokens and needs @@ -439,8 +439,9 @@ def test_analyst_creates_insight_end_to_end(platform_server: str) -> None: # no result = subprocess.run( _cli_cmd( "nemo", - "insights", - "analyze", + "agents", + "analyst", + "run", "--agent", TEST_AGENT, "--workspace", diff --git a/plugins/nemo-insights/src/nemo_insights_plugin/analyst/cli.py b/plugins/nemo-insights/src/nemo_insights_plugin/analyst/cli.py index 8196eeb2b1..454f83ced0 100644 --- a/plugins/nemo-insights/src/nemo_insights_plugin/analyst/cli.py +++ b/plugins/nemo-insights/src/nemo_insights_plugin/analyst/cli.py @@ -3,9 +3,10 @@ """Analyst CLI — ``nemo agents analyst ...`` subcommands. -The verbs are the module-level callbacks that back ``nemo insights analyze`` -and ``nemo insights doctor``, so the two command trees cannot drift. The -periodic-analysis and job surfaces stay on ``nemo insights``: they manage the +Registered under ``nemo.cli.agents`` and mounted by ``AgentsCLI`` as +``nemo agents analyst ``. Verb bodies live as module-level callbacks in +``nemo_insights_plugin.cli`` so the analyst implementation stays in one place. +Periodic-analysis and job surfaces stay on ``nemo insights``: they manage the plugin's scheduled runs rather than driving the analyst itself. """ diff --git a/plugins/nemo-insights/src/nemo_insights_plugin/cli.py b/plugins/nemo-insights/src/nemo_insights_plugin/cli.py index cb110bd628..83ae80da1f 100644 --- a/plugins/nemo-insights/src/nemo_insights_plugin/cli.py +++ b/plugins/nemo-insights/src/nemo_insights_plugin/cli.py @@ -3,10 +3,10 @@ """Insights CLI and contributed subcommands. -The module-level :func:`analyze` and :func:`doctor` callbacks are shared with -:class:`nemo_insights_plugin.analyst.cli.AnalystCLI`, which mounts them as the canonical -``nemo agents analyst run`` and ``nemo agents analyst doctor``. They stay registered here -as ``nemo insights analyze`` / ``nemo insights doctor`` for backward compatibility. +The module-level :func:`analyze` and :func:`doctor` callbacks are the verb bodies for +:class:`nemo_insights_plugin.analyst.cli.AnalystCLI` (``nemo agents analyst run`` / +``nemo agents analyst doctor``). This module's ``InsightsCLI`` keeps the periodic +``analysis`` surface and does not mount those agent verbs. """ import asyncio @@ -338,9 +338,6 @@ def _root() -> None: ) app.add_typer(analysis_app, name="analysis") - app.command("analyze")(analyze) - app.command("doctor")(doctor) - @analysis_app.command("enable") def enable_analysis( agent: str = typer.Option( diff --git a/plugins/nemo-insights/testbed/README.md b/plugins/nemo-insights/testbed/README.md index a547fbc625..07758f9aaa 100644 --- a/plugins/nemo-insights/testbed/README.md +++ b/plugins/nemo-insights/testbed/README.md @@ -4,7 +4,7 @@ # testbed — insights analyst test runner (maintainer tooling) Runs the Insights analyst against registered **subjects** and emits Insights. Think -"pytest for the analysis loop." This is dev tooling — it *drives* `nemo insights`; +"pytest for the analysis loop." This is dev tooling — it *drives* `nemo agents analyst`; it is not the product CLI and is not shipped in the wheel. ```bash diff --git a/plugins/nemo-insights/testbed/cli.py b/plugins/nemo-insights/testbed/cli.py index d781155dfc..fc3c6a9699 100644 --- a/plugins/nemo-insights/testbed/cli.py +++ b/plugins/nemo-insights/testbed/cli.py @@ -29,7 +29,7 @@ bundles (state-v1..v5) are restorable only from a pre-migration checkout; see testbed/README.md. -This drives the analyst (`nemo insights analyze`) against registered subjects; it is not +This drives the analyst (`nemo agents analyst run`) against registered subjects; it is not the product CLI and is not shipped in the wheel. """ diff --git a/plugins/nemo-insights/tests/test_cli_profile.py b/plugins/nemo-insights/tests/test_cli_profile.py index 418fcece24..82141b85fd 100644 --- a/plugins/nemo-insights/tests/test_cli_profile.py +++ b/plugins/nemo-insights/tests/test_cli_profile.py @@ -9,6 +9,7 @@ import pytest import typer from nemo_insights_plugin import cli +from nemo_insights_plugin.analyst.cli import AnalystCLI from nemo_insights_plugin.contracts.profile import DEFAULT_BASE_URL from nemo_insights_plugin.preflight import AnalysisProbes from nemo_platform import NeMoPlatformError @@ -30,7 +31,7 @@ async def __call__(self, **kwargs: object) -> str: @pytest.fixture def app() -> typer.Typer: - return cli.InsightsCLI().get_cli() + return AnalystCLI().get_cli() @pytest.fixture(autouse=True) @@ -82,7 +83,7 @@ def test_analyze_runs_flag_free_from_profile(app: typer.Typer, profile_tree: Pat monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -97,7 +98,7 @@ def test_analyze_flags_override_profile(app: typer.Typer, profile_tree: Path, mo monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze", "--agent", "other", "--workspace", "other-ws"]) + result = runner.invoke(app, ["run", "--agent", "other", "--workspace", "other-ws"]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -112,7 +113,7 @@ def test_profile_env_is_loaded_before_base_url_resolution(app: typer.Typer, prof (profile_tree / ".env").write_text("NMP_BASE_URL=https://platform.example\n", encoding="utf-8") monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -145,7 +146,7 @@ async def record_workspace_probe(base_url: str, workspace: str, agent: str) -> b env_file.write_bytes(b"KEY=\xff") monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 1 error_lines = [line for line in result.stderr.splitlines() if line.startswith("Error:")] @@ -287,7 +288,7 @@ def test_base_url_precedence_uses_only_nmp_base_url( monkeypatch.setenv(name, value) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze", *arguments]) + result = runner.invoke(app, ["run", *arguments]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -301,7 +302,7 @@ def test_explicit_profile_is_used_outside_profile_directory( monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze", "--profile", str(profile_tree / "optimizer.yaml")]) + result = runner.invoke(app, ["run", "--profile", str(profile_tree / "optimizer.yaml")]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -313,7 +314,7 @@ def test_malformed_explicit_profile_errors(app: typer.Typer, tmp_path: Path, mon profile.write_text("agent: ''\n", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze", "--profile", str(profile)]) + result = runner.invoke(app, ["run", "--profile", str(profile)]) assert result.exit_code != 0 assert "Invalid profile" in result.output @@ -327,7 +328,7 @@ def test_malformed_discovered_profile_warns_when_flags_are_complete( (tmp_path / "optimizer.yaml").write_text("agent: ''\n", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze", "--agent", "other", "--workspace", "other-ws"]) + result = runner.invoke(app, ["run", "--agent", "other", "--workspace", "other-ws"]) assert result.exit_code == 0, result.output assert "warning:" in result.output @@ -340,7 +341,7 @@ def test_malformed_discovered_profile_warns_with_agent_only(app: typer.Typer, tm (tmp_path / "optimizer.yaml").write_text("agent: ''\n", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze", "--agent", "other"]) + result = runner.invoke(app, ["run", "--agent", "other"]) assert result.exit_code == 0, result.output assert "warning:" in result.output @@ -357,7 +358,7 @@ def test_malformed_discovered_profile_still_loads_env_file(app: typer.Typer, tmp (tmp_path / ".env").write_text("INFERENCE_API_KEY=from-env-file\n", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze", "--agent", "other"]) + result = runner.invoke(app, ["run", "--agent", "other"]) assert result.exit_code == 0, result.output assert os.environ["INFERENCE_API_KEY"] == "from-env-file" @@ -370,7 +371,7 @@ def test_malformed_discovered_profile_errors_without_agent(app: typer.Typer, tmp (tmp_path / "optimizer.yaml").write_text("agent: ''\n", encoding="utf-8") monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze", "--workspace", "other-ws"]) + result = runner.invoke(app, ["run", "--workspace", "other-ws"]) assert result.exit_code != 0 assert "Invalid profile" in result.output @@ -385,7 +386,7 @@ def test_explicit_output_overrides_profile_default( monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze", "--insights-file-output", str(output)]) + result = runner.invoke(app, ["run", "--insights-file-output", str(output)]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -395,7 +396,7 @@ def test_explicit_output_overrides_profile_default( def test_missing_profile_and_agent_errors(app: typer.Typer, tmp_path: Path, monkeypatch) -> None: monkeypatch.chdir(tmp_path) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code != 0 assert "No --agent given and no optimizer.yaml profile found" in result.output @@ -417,7 +418,7 @@ def test_analyze_blocks_before_runner_when_preflight_fails( ) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 1 assert "INFERENCE_API_KEY not set" in result.output @@ -446,7 +447,7 @@ async def record_workspace_probe(base_url: str, workspace: str, agent: str) -> b ) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 0, result.output assert recorder.kwargs is not None @@ -472,7 +473,7 @@ async def fail_analysis(**kwargs: object) -> str: monkeypatch.setattr(cli, "run_analyst", fail_analysis) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 1 error_lines = [line for line in result.stderr.splitlines() if line.startswith("Error:")] @@ -493,7 +494,7 @@ async def fail_analysis(**kwargs: object) -> str: monkeypatch.setattr(cli, "run_analyst", fail_analysis) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert result.exit_code == 1 error_lines = [line for line in result.stderr.splitlines() if line.startswith("Error:")] @@ -523,7 +524,7 @@ def fail_to_construct(base_url: str | None) -> object: monkeypatch.setattr(cli, "make_client", fail_to_construct) monkeypatch.chdir(profile_tree) - result = runner.invoke(app, ["analyze"]) + result = runner.invoke(app, ["run"]) assert attempts == 1 assert result.exit_code == 1 @@ -563,7 +564,7 @@ def test_analyze_rejects_invalid_existing_insights_file_before_runner( output.write_bytes(payload) monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(profile_tree) - arguments = ["analyze", "--insights-file-output", str(output)] if explicit else ["analyze"] + arguments = ["run", "--insights-file-output", str(output)] if explicit else ["run"] result = runner.invoke(app, arguments) @@ -606,7 +607,7 @@ def test_analyze_rejects_invalid_insights_records_before_runner( output.write_bytes(payload) monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(profile_tree) - arguments = ["analyze", "--insights-file-output", str(output)] if explicit else ["analyze"] + arguments = ["run", "--insights-file-output", str(output)] if explicit else ["run"] result = runner.invoke(app, arguments) @@ -633,7 +634,7 @@ def test_analyze_accepts_existing_insights_file_without_insights_key( output.write_text("metadata: retained\n", encoding="utf-8") monkeypatch.setattr(cli, "run_analyst", recorder) monkeypatch.chdir(profile_tree) - arguments = ["analyze", "--insights-file-output", str(output)] if explicit else ["analyze"] + arguments = ["run", "--insights-file-output", str(output)] if explicit else ["run"] result = runner.invoke(app, arguments) @@ -641,7 +642,7 @@ def test_analyze_accepts_existing_insights_file_without_insights_key( assert recorder.kwargs is not None -@pytest.mark.parametrize("command", ["doctor", "analyze"]) +@pytest.mark.parametrize("command", ["doctor", "run"]) def test_commands_reject_invalid_utf8_agent_spec( app: typer.Typer, profile_tree: Path, @@ -659,7 +660,7 @@ def test_commands_reject_invalid_utf8_agent_spec( assert "Traceback" not in result.output -@pytest.mark.parametrize("command", ["doctor", "analyze"]) +@pytest.mark.parametrize("command", ["doctor", "run"]) def test_commands_reject_unreadable_agent_spec( app: typer.Typer, profile_tree: Path, diff --git a/web/packages/studio/src/routes/optimizer/InsightOpenModal/command.ts b/web/packages/studio/src/routes/optimizer/InsightOpenModal/command.ts index 72fdade41c..f14bc29b8b 100644 --- a/web/packages/studio/src/routes/optimizer/InsightOpenModal/command.ts +++ b/web/packages/studio/src/routes/optimizer/InsightOpenModal/command.ts @@ -4,7 +4,7 @@ const shellQuote = (value: string): string => value.replace(/'/g, "'\\''"); export const buildOptimizerExperimentCommand = (insightId: string, workspace: string): string => - `nemo optimizer experiment \\\n` + + `nemo agents experimentalist run \\\n` + ` --insight '${shellQuote(insightId)}' \\\n` + ` --train-dataset "" \\\n` + ` --validation-dataset "" \\\n` + diff --git a/web/packages/studio/src/routes/optimizer/InsightOpenModal/index.test.ts b/web/packages/studio/src/routes/optimizer/InsightOpenModal/index.test.ts index 94576a5711..38458ef53f 100644 --- a/web/packages/studio/src/routes/optimizer/InsightOpenModal/index.test.ts +++ b/web/packages/studio/src/routes/optimizer/InsightOpenModal/index.test.ts @@ -4,11 +4,11 @@ import { buildOptimizerExperimentCommand } from '@studio/routes/optimizer/InsightOpenModal/command'; describe('buildOptimizerExperimentCommand', () => { - it('uses the optimizer experiment contract and quotes the insight and workspace', () => { + it('uses the experimentalist run contract and quotes the insight and workspace', () => { const command = buildOptimizerExperimentCommand("insight-'quoted", "workspace-'quoted"); expect(command).toBe( - 'nemo optimizer experiment \\\n' + + 'nemo agents experimentalist run \\\n' + " --insight 'insight-'\\''quoted' \\\n" + ' --train-dataset "" \\\n' + ' --validation-dataset "" \\\n' +