Compare commits

...

1 Commits

Author SHA1 Message Date
HAL9000 8f9b02af4a fix(cli): remove positional NAME from agents actor add — read name from YAML file
Remove the `NAME` positional argument from `agents actor add`. The actor
name is now read from the `name` field in the config file specified with
`--config`. If no `name` field exists, a clear `BadParameter` error guides
the user to correct the config. Updated all BDD step definitions to match
and cleaned up unused local variables.

ISSUES CLOSED: #8640
2026-05-07 20:12:23 +00:00
10 changed files with 87 additions and 170 deletions
+7
View File
@@ -20,6 +20,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- **`agents actor add` reads name from config file instead of positional argument** (#8640): The
``NAME`` positional argument has been removed from the ``agents actor add`` command. The actor
name is now read from the ``name`` field in the YAML/JSON configuration file specified with
``--config``. If no ``name`` field exists in the config, a clear error message is shown. This
simplifies the CLI interface by having the config file be the single source of truth for actor
settings including the namespaced name (e.g., ``local/my-actor``). Examples: `agents actor add --config ./actors/my-actor.yaml`.
- **`agents session list` now displays full 26-character session ULIDs** (#10970): The Rich table
and Summary panel ("Most Recent" / "Oldest") previously showed only the first 8 characters of
each session ULID. This made the output unusable for copy-paste into `session tell`,
+1
View File
@@ -21,6 +21,7 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
* HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes.
* HAL 9000 has contributed the agent-evolution-pool-supervisor PR metadata assignment (#7888): the supervisor now automatically looks up the Type/Automation label and earliest open milestone before dispatching improvement PR creation workers, ensuring all generated improvement PRs have correct Type labels and milestone assignments.
* HAL 9000 has contributed the actors actor add positional NAME removal (PR #8640): removed the required positional NAME argument from `agents actor add`, now reads name from the config file's `name` field with a clear error message when missing.
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559).
+12 -20
View File
@@ -1,24 +1,16 @@
Feature: agents actor add NAME positional argument
Feature: agents actor add reads name from config file
As a user of the CleverAgents CLI
I want to pass the actor name as a positional argument to `agents actor add`
So that the CLI matches the spec synopsis: agents actor add <NAME> --config <FILE>
I want to read the actor name from the YAML/JSON config file instead of passing it as a positional argument
So that the CLI uses the config file for all actor configuration including the namespaced name
@tdd_issue @tdd_issue_4230 @tdd_expected_fail
Scenario: actor add accepts NAME as positional argument
Scenario: actor add reads name from config file successfully
Given an actor CLI runner
And I have an actor JSON config file with a name field
When I run actor add with config file
Then the actor add should succeed using the name from the config file
Scenario: actor add fails without name in config file
Given an actor CLI runner
And I have an actor JSON config file without a name field
When I run actor add with NAME positional argument and config
Then the actor add should succeed with the positional name
@tdd_issue @tdd_issue_4230 @tdd_expected_fail
Scenario: actor add NAME positional argument takes precedence over config name
Given an actor CLI runner
And I have an actor JSON config file with a different name
When I run actor add with NAME positional argument overriding config name
Then the actor add should use the positional NAME not the config name
Scenario: actor add without NAME positional argument fails
Given an actor CLI runner
And I have an actor JSON config file without a name field
When I run actor add with config but no NAME positional argument
Then the actor command should fail with missing argument error
When I run actor add with config file but no name
Then the actor add should fail with an error about missing name
@@ -1,4 +1,4 @@
"""Step definitions for actor add NAME positional argument feature."""
"""Step definitions for actor add name-from-config feature."""
from __future__ import annotations
@@ -43,6 +43,23 @@ def _register_cleanup(context: Any, path: Path) -> None:
context._cleanup_handlers.append(lambda: path.unlink(missing_ok=True))
@given("I have an actor JSON config file with a name field")
def step_impl(context: Any) -> None:
context.actor_config_data = {
"name": "local/my-actor",
"provider": "openai",
"model": "gpt-4o-mini",
"temperature": 0.5,
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@given("I have an actor JSON config file without a name field")
def step_impl(context: Any) -> None:
context.actor_config_data = {
@@ -59,26 +76,12 @@ def step_impl(context: Any) -> None:
_register_cleanup(context, context.actor_config_path)
@given("I have an actor JSON config file with a different name")
@when("I run actor add with config file")
def step_impl(context: Any) -> None:
context.actor_config_data = {
"name": "local/config-name-actor",
"provider": "openai",
"model": "gpt-4o-mini",
}
with tempfile.NamedTemporaryFile(
delete=False, suffix=".json", mode="w", encoding="utf-8"
) as handle:
json.dump(context.actor_config_data, handle)
handle.flush()
context.actor_config_path = Path(handle.name)
_register_cleanup(context, context.actor_config_path)
@when("I run actor add with NAME positional argument and config")
def step_impl(context: Any) -> None:
context.positional_name = "local/my-actor"
mock_actor = _make_actor(name=context.positional_name)
config_name = context.actor_config_data.get(
"name", context.actor_config_data.get("_fallback_name", "local/default-actor")
)
mock_actor = _make_actor(name=config_name)
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = mock_actor
@@ -87,7 +90,6 @@ def step_impl(context: Any) -> None:
actor_app,
[
"add",
context.positional_name,
"--config",
str(context.actor_config_path),
],
@@ -95,28 +97,9 @@ def step_impl(context: Any) -> None:
context.mock_registry = registry
@when("I run actor add with NAME positional argument overriding config name")
def step_impl(context: Any) -> None:
context.positional_name = "local/positional-name-actor"
mock_actor = _make_actor(name=context.positional_name)
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
registry.upsert_actor.return_value = mock_actor
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
[
"add",
context.positional_name,
"--config",
str(context.actor_config_path),
],
)
context.mock_registry = registry
@when("I run actor add with config but no NAME positional argument")
@when("I run actor add with config file but no name")
def step_impl(context: Any) -> None:
mock_actor = _make_actor()
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
mock_svc.return_value = (MagicMock(), registry)
@@ -130,13 +113,13 @@ def step_impl(context: Any) -> None:
)
@then("the actor add should succeed with the positional name")
@then("the actor add should succeed using the name from the config file")
def step_impl(context: Any) -> None:
assert context.result.exit_code == 0, (
f"Expected exit_code=0, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)
# Verify the registry was called with the positional name
# Verify the registry was called with the name from config file
assert context.mock_registry.upsert_actor.called, (
"Expected upsert_actor to be called on the registry"
)
@@ -144,34 +127,19 @@ def step_impl(context: Any) -> None:
actual_name = call_kwargs.kwargs.get("name") or (
call_kwargs.args[0] if call_kwargs.args else None
)
assert actual_name == context.positional_name, (
f"Expected upsert_actor called with name={context.positional_name!r}, "
expected_name = context.actor_config_data.get("name", "local/default-actor")
assert actual_name == expected_name, (
f"Expected upsert_actor called with name={expected_name!r}, "
f"got name={actual_name!r}"
)
@then("the actor add should use the positional NAME not the config name")
def step_impl(context: Any) -> None:
assert context.result.exit_code == 0, (
f"Expected exit_code=0, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)
assert context.mock_registry.upsert_actor.called, (
"Expected upsert_actor to be called on the registry"
)
call_kwargs = context.mock_registry.upsert_actor.call_args
actual_name = call_kwargs.kwargs.get("name") or (
call_kwargs.args[0] if call_kwargs.args else None
)
assert actual_name == context.positional_name, (
f"Expected upsert_actor called with positional name={context.positional_name!r}, "
f"but got name={actual_name!r} (config name was 'local/config-name-actor')"
)
@then("the actor command should fail with missing argument error")
@then("the actor add should fail with an error about missing name")
def step_impl(context: Any) -> None:
assert context.result.exit_code != 0, (
f"Expected non-zero exit_code, got {context.result.exit_code}.\n"
f"Output:\n{context.result.output}"
)
assert "name" in context.result.output.lower(), (
f"Expected error message to mention 'name', got:\n{context.result.output}"
)
@@ -126,7 +126,6 @@ def step_impl(context: Any) -> None:
@when("I run actor add with the typed config")
def step_impl(context: Any) -> None:
actor_name = context.actor_config_data.get("name", "local/rich-actor")
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
# registry.add() is the YAML-first path used by the add command
@@ -134,13 +133,12 @@ def step_impl(context: Any) -> None:
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
["add", actor_name, "--config", str(context.actor_config_path)],
["add", "--config", str(context.actor_config_path)],
)
@when("I run actor add with the capabilities config")
def step_impl(context: Any) -> None:
actor_name = context.actor_config_data.get("name", "local/cap-actor")
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
# registry.add() is the YAML-first path used by the add command
@@ -148,13 +146,12 @@ def step_impl(context: Any) -> None:
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
["add", actor_name, "--config", str(context.actor_config_path)],
["add", "--config", str(context.actor_config_path)],
)
@when("I run actor add with the tools config")
def step_impl(context: Any) -> None:
actor_name = context.actor_config_data.get("name", "local/tool-actor")
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
# registry.add() is the YAML-first path used by the add command
@@ -162,13 +159,12 @@ def step_impl(context: Any) -> None:
mock_svc.return_value = (MagicMock(), registry)
context.result = context.runner.invoke(
actor_app,
["add", actor_name, "--config", str(context.actor_config_path)],
["add", "--config", str(context.actor_config_path)],
)
@when("I run actor add with the typed config in json format")
def step_impl(context: Any) -> None:
actor_name = context.actor_config_data.get("name", "local/rich-actor")
with patch("cleveragents.cli.commands.actor._get_services") as mock_svc:
registry = MagicMock()
# registry.add() is the YAML-first path used by the add command
@@ -178,7 +174,6 @@ def step_impl(context: Any) -> None:
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--format",
@@ -424,7 +424,7 @@ def step_run_actor_add(context: Context, name: str) -> None:
mock_get_services.return_value = (mock_service, mock_registry)
result = runner.invoke(
actor_app,
["add", name, "--config", str(context.config_file)],
["add", "--config", str(context.config_file)],
catch_exceptions=True,
)
@@ -903,7 +903,7 @@ def step_run_actor_add_no_name(context: Context) -> None:
mock_get_services.return_value = (mock_service, mock_registry)
result = runner.invoke(
actor_app,
["add", name, "--config", str(context.config_file)],
["add", "--config", str(context.config_file)],
catch_exceptions=True,
)
@@ -63,7 +63,7 @@ def step_run_actor_add_yaml_first(context: Any) -> None:
context.result = context.runner.invoke(
actor_app,
["add", "local/test-actor", "--config", str(context.actor_config_path)],
["add", "--config", str(context.actor_config_path)],
)
context.mock_actor_registry = mock_registry
@@ -86,10 +86,9 @@ def step_run_actor_add_update_yaml_first(context: Any) -> None:
actor_app,
[
"add",
"local/test-actor",
"--config",
str(context.actor_config_path),
"--update",
"--update","
],
)
context.mock_actor_registry = mock_registry
@@ -115,10 +114,9 @@ def step_run_actor_add_set_default_yaml_first(context: Any) -> None:
actor_app,
[
"add",
"local/test-actor",
"--config",
str(context.actor_config_path),
"--set-default",
"--set-default","
],
)
context.mock_actor_registry = mock_registry
@@ -146,7 +144,6 @@ def step_run_actor_add_option_override_yaml_first(context: Any) -> None:
actor_app,
[
"add",
"local/test-actor",
"--config",
str(context.actor_config_path),
"--option",
+8 -47
View File
@@ -193,7 +193,7 @@ def step_impl(context):
mock_actor_service = MagicMock()
mock_actor_registry = MagicMock()
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
context.result = context.runner.invoke(actor_app, ["add", "local/test-actor"])
context.result = context.runner.invoke(actor_app, ["add"])
context.mock_actor_registry = mock_actor_registry
context.expected_error = "Config file is required"
@@ -216,16 +216,11 @@ def step_impl(context):
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
@@ -260,12 +255,11 @@ def step_impl(context):
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = context.actor_config_data.get("name", "local/test-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
*option_args,
@@ -289,12 +283,11 @@ def step_impl(context):
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = context.actor_config_data.get("name", "local/test-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
*option_args,
@@ -318,12 +311,11 @@ def step_impl(context):
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = context.actor_config_data.get("name", "local/test-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
*option_args,
@@ -335,18 +327,12 @@ def step_impl(context):
@when("I run actor add with malformed option")
def step_impl(context):
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_get_services.return_value = (MagicMock(), MagicMock())
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--option",
@@ -358,18 +344,12 @@ def step_impl(context):
@when("I run actor add with empty option key")
def step_impl(context):
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
with patch("cleveragents.cli.commands.actor._get_services") as mock_get_services:
mock_get_services.return_value = (MagicMock(), MagicMock())
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--option",
@@ -397,16 +377,11 @@ def step_impl(context):
# Return the mocked services
mock_get_services.return_value = (mock_actor_service, mock_actor_registry)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--unsafe",
@@ -435,12 +410,10 @@ def step_impl(context):
)
mock_get_services.return_value = (actor_service, None)
actor_name = context.actor_config_data.get("name", "service/graph-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
@@ -459,7 +432,6 @@ def step_impl(context):
actor_app,
[
"add",
"local/test-actor",
"--config",
str(missing_path),
],
@@ -493,7 +465,6 @@ def step_impl(context):
actor_app,
[
"add",
"local/test-actor",
"--config",
str(config_path),
],
@@ -507,16 +478,11 @@ def step_impl(context):
actor_service = MagicMock()
mock_get_services.return_value = (actor_service, None)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
@@ -551,16 +517,11 @@ def step_impl(context):
actor_service = MagicMock()
mock_get_services.return_value = (actor_service, None)
actor_name = (
context.actor_config_data.get("name", "local/test-actor")
if isinstance(context.actor_config_data, dict)
else "local/test-actor"
)
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
],
-6
View File
@@ -216,12 +216,10 @@ def step_add_update(context: Any) -> None:
# registry.add() is the YAML-first path used by the add command
mock_registry.add.return_value = mock_actor
mock_svc.return_value = (mock_service, mock_registry)
actor_name = context.actor_config_data.get("name", "local/test-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--update",
@@ -251,12 +249,10 @@ def step_add_format_json(context: Any) -> None:
# registry.add() is the YAML-first path used by the add command
mock_registry.add.return_value = mock_actor
mock_svc.return_value = (mock_service, mock_registry)
actor_name = context.actor_config_data.get("name", "local/test-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--format",
@@ -274,12 +270,10 @@ def step_add_format_yaml(context: Any) -> None:
# registry.add() is the YAML-first path used by the add command
mock_registry.add.return_value = mock_actor
mock_svc.return_value = (mock_service, mock_registry)
actor_name = context.actor_config_data.get("name", "local/test-actor")
context.result = context.runner.invoke(
actor_app,
[
"add",
actor_name,
"--config",
str(context.actor_config_path),
"--format",
+17 -15
View File
@@ -533,13 +533,6 @@ def _print_role_warnings(config_blob: dict[str, Any]) -> None:
@app.command()
def add(
name: Annotated[
str,
typer.Argument(
help="Namespaced actor name (e.g. local/my-actor)",
metavar="NAME",
),
],
config: Annotated[
Path | None,
typer.Option("--config", "-c", help="Path to JSON/YAML actor config"),
@@ -569,19 +562,18 @@ def add(
) -> None:
"""Add a new actor configuration.
The actor name is provided as a required positional argument. The YAML/JSON
configuration file specified with ``--config`` supplies the actor settings.
The positional ``NAME`` takes precedence over any ``name`` field in the
config file.
The actor name is read from the ``name`` field in the YAML/JSON
configuration file specified with ``--config``. The config file supplies
all actor settings including the namespaced name.
Signature:
``agents actor add <NAME> --config <FILE> [--update] [--unsafe]
``agents actor add --config <FILE> [--update] [--unsafe]
[--set-default] [--option key=value] [--format FORMAT]``
Examples:
agents actor add local/my-actor --config ./actors/my-actor.yaml
agents actor add local/my-actor --config ./actors/my-actor.yaml --update
agents actor add local/my-actor --config actor.yaml --format json
agents actor add --config ./actors/my-actor.yaml
agents actor add --config ./actors/my-actor.yaml --update
agents actor add --config actor.yaml --format json
"""
service, registry = _get_services()
option_overrides = _parse_option_overrides(option)
@@ -597,6 +589,16 @@ def add(
assert loaded is not None, "unreachable: config is not None"
yaml_text, config_blob = loaded
# Read actor name from config file.
if not config_blob.get("name"):
raise typer.BadParameter(
f"Actor config must contain a 'name' field. "
f"Use a namespaced format like 'local/my-actor'.
"
f"Config file: {config}"
)
name = config_blob["name"]
# Validate v3 config via ActorConfigSchema if detected.
# This ensures v3 actors are fully validated (cycle detection, required
# fields, enum values) before the registry stores them.