feat(cli): add automation-profile commands
CI / lint (pull_request) Successful in 14s
CI / security (pull_request) Successful in 28s
CI / typecheck (pull_request) Successful in 30s
CI / quality (pull_request) Successful in 32s
CI / build (pull_request) Successful in 15s
CI / integration_tests (pull_request) Successful in 4m38s
CI / unit_tests (pull_request) Successful in 18m27s
CI / docker (pull_request) Successful in 41s
CI / coverage (pull_request) Successful in 1h10m6s
CI / lint (pull_request) Successful in 14s
CI / security (pull_request) Successful in 28s
CI / typecheck (pull_request) Successful in 30s
CI / quality (pull_request) Successful in 32s
CI / build (pull_request) Successful in 15s
CI / integration_tests (pull_request) Successful in 4m38s
CI / unit_tests (pull_request) Successful in 18m27s
CI / docker (pull_request) Successful in 41s
CI / coverage (pull_request) Successful in 1h10m6s
Add CLI command group `agents automation-profile` with add, remove, list, and show subcommands for managing automation profiles that control plan execution autonomy. Changes: - New `automation_profile.py` CLI module with YAML config input, schema_version guard, namespaced name validation, --update support, and all output formats (json/yaml/plain/table/rich) - Register automation-profile in main CLI app - Add deprecation warnings for --automation-level on plan use and set-automation-level commands - Update CLI reference docs with command examples, built-in profiles list, and deprecation notes - 26 Behave scenarios covering all commands and error paths - 9 Robot Framework integration smoke tests - ASV benchmarks for CLI parsing performance - Mark A6.cli items complete in implementation_plan.md
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""ASV benchmarks for Automation Profile CLI command throughput.
|
||||
|
||||
Measures the performance of:
|
||||
- Add profile from YAML config
|
||||
- List profiles rendering
|
||||
- Show profile rendering
|
||||
- Remove profile
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
# Ensure the local *source* tree is importable even when ASV has an
|
||||
# older build of the package installed.
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
import cleveragents # noqa: E402
|
||||
|
||||
importlib.reload(cleveragents)
|
||||
|
||||
from typer.testing import CliRunner # noqa: E402
|
||||
|
||||
from cleveragents.cli.commands.automation_profile import ( # noqa: E402
|
||||
_InMemoryProfileRepository,
|
||||
app as profile_app,
|
||||
)
|
||||
from cleveragents.domain.models.core.automation_profile import ( # noqa: E402
|
||||
AutomationProfile,
|
||||
)
|
||||
|
||||
_VALID_YAML = """\
|
||||
name: bench/test-profile
|
||||
description: Benchmark test profile
|
||||
schema_version: "1.0"
|
||||
auto_strategize: 0.5
|
||||
auto_execute: 0.5
|
||||
auto_apply: 1.0
|
||||
auto_decisions_strategize: 0.5
|
||||
auto_decisions_execute: 0.5
|
||||
auto_validation_fix: 0.5
|
||||
auto_strategy_revision: 0.5
|
||||
auto_reversion_from_apply: 0.5
|
||||
auto_child_plans: 0.5
|
||||
auto_retry_transient: 0.0
|
||||
auto_checkpoint_restore: 0.5
|
||||
require_sandbox: true
|
||||
require_checkpoints: true
|
||||
allow_unsafe_tools: false
|
||||
"""
|
||||
|
||||
_runner = CliRunner()
|
||||
|
||||
|
||||
def _reset_repo() -> None:
|
||||
"""Reset the module-level in-memory repo."""
|
||||
import cleveragents.cli.commands.automation_profile as ap_mod
|
||||
|
||||
ap_mod._repo = _InMemoryProfileRepository()
|
||||
|
||||
|
||||
class AutomationProfileCLIAddSuite:
|
||||
"""Benchmark automation-profile add --config throughput."""
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Write a temporary YAML file."""
|
||||
_reset_repo()
|
||||
fd, self._path = tempfile.mkstemp(suffix=".yaml")
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(_VALID_YAML)
|
||||
|
||||
def teardown(self) -> None:
|
||||
"""Clean up."""
|
||||
Path(self._path).unlink(missing_ok=True)
|
||||
|
||||
def time_add_from_config(self) -> None:
|
||||
"""Benchmark add --config end-to-end."""
|
||||
_reset_repo()
|
||||
_runner.invoke(profile_app, ["add", "--config", self._path])
|
||||
|
||||
|
||||
class AutomationProfileCLIListSuite:
|
||||
"""Benchmark automation-profile list throughput."""
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up with built-in profiles."""
|
||||
_reset_repo()
|
||||
|
||||
def time_list_all(self) -> None:
|
||||
"""Benchmark listing all profiles."""
|
||||
_runner.invoke(profile_app, ["list"])
|
||||
|
||||
def time_list_json(self) -> None:
|
||||
"""Benchmark listing profiles as JSON."""
|
||||
_runner.invoke(profile_app, ["list", "--format", "json"])
|
||||
|
||||
def time_list_with_regex(self) -> None:
|
||||
"""Benchmark listing with regex filter."""
|
||||
_runner.invoke(profile_app, ["list", "caut.*"])
|
||||
|
||||
|
||||
class AutomationProfileCLIShowSuite:
|
||||
"""Benchmark automation-profile show throughput."""
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up."""
|
||||
_reset_repo()
|
||||
|
||||
def time_show_rich(self) -> None:
|
||||
"""Benchmark showing a profile in rich format."""
|
||||
_runner.invoke(profile_app, ["show", "manual"])
|
||||
|
||||
def time_show_json(self) -> None:
|
||||
"""Benchmark showing a profile in JSON format."""
|
||||
_runner.invoke(profile_app, ["show", "manual", "--format", "json"])
|
||||
|
||||
def time_show_yaml(self) -> None:
|
||||
"""Benchmark showing a profile in YAML format."""
|
||||
_runner.invoke(profile_app, ["show", "manual", "--format", "yaml"])
|
||||
@@ -108,3 +108,80 @@ allow_unsafe_tools: false
|
||||
```
|
||||
|
||||
See `docs/schema/automation_profile.schema.yaml` for the full YAML schema and `examples/profiles/` for example configurations.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
The `agents automation-profile` command group manages automation profiles
|
||||
from the terminal.
|
||||
|
||||
### `automation-profile add`
|
||||
|
||||
Register a custom profile from a YAML configuration file:
|
||||
|
||||
```bash
|
||||
agents automation-profile add --config ./profiles/strict.yaml
|
||||
agents automation-profile add --config ./profiles/strict.yaml --update
|
||||
agents automation-profile add --config ./profiles/strict.yaml --format json
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--config / -c` — Path to the YAML configuration file (required).
|
||||
- `--update` — Overwrite an existing custom profile instead of raising a conflict error.
|
||||
- `--format / -f` — Output format: `json`, `yaml`, `plain`, `table`, or `rich` (default: `rich`).
|
||||
|
||||
### `automation-profile remove`
|
||||
|
||||
Remove a custom profile by name (built-in profiles cannot be removed):
|
||||
|
||||
```bash
|
||||
agents automation-profile remove acme/strict --yes
|
||||
agents automation-profile remove acme/strict --yes --format json
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--yes / -y` — Skip the interactive confirmation prompt.
|
||||
- `--format / -f` — Output format.
|
||||
|
||||
### `automation-profile list`
|
||||
|
||||
List all profiles with optional filtering:
|
||||
|
||||
```bash
|
||||
agents automation-profile list
|
||||
agents automation-profile list --namespace acme
|
||||
agents automation-profile list "caut.*"
|
||||
agents automation-profile list --format json
|
||||
```
|
||||
|
||||
Options:
|
||||
- *regex* (positional) — Optional regex pattern to filter profile names.
|
||||
- `--namespace / -n` — Filter by the namespace portion of namespaced names.
|
||||
- `--format / -f` — Output format.
|
||||
|
||||
### `automation-profile show`
|
||||
|
||||
Display full details for a single profile:
|
||||
|
||||
```bash
|
||||
agents automation-profile show manual
|
||||
agents automation-profile show acme/strict --format yaml
|
||||
```
|
||||
|
||||
Options:
|
||||
- `--format / -f` — Output format.
|
||||
|
||||
## Deprecation: `--automation-level`
|
||||
|
||||
The `--automation-level` option on `plan use` and the `plan set-automation-level`
|
||||
command are **deprecated** and will be removed in a future release.
|
||||
|
||||
Legacy automation levels are mapped to built-in profiles:
|
||||
|
||||
| Legacy level | Built-in profile |
|
||||
|-------------|-----------------|
|
||||
| `manual` | `manual` |
|
||||
| `supervised` | `supervised` |
|
||||
| `auto` | `auto` |
|
||||
| `full_auto` | `full-auto` |
|
||||
|
||||
Use `--automation-profile <name>` instead of `--automation-level`.
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
Feature: Automation Profile CLI commands
|
||||
As a developer
|
||||
I want to manage automation profiles via CLI commands
|
||||
So that I can control plan execution autonomy levels
|
||||
|
||||
Background:
|
||||
Given an automation profile CLI runner with mocks
|
||||
|
||||
# Add command tests
|
||||
Scenario: Add profile via config file
|
||||
Given a valid automation profile config YAML file
|
||||
When I run automation-profile add with --config pointing to the YAML file
|
||||
Then the automation-profile add should succeed
|
||||
And the automation-profile output should contain "acme/strict"
|
||||
|
||||
Scenario: Add profile with --update for existing custom profile
|
||||
Given a valid automation profile config YAML file
|
||||
And the custom profile "acme/strict" already exists
|
||||
When I run automation-profile add with --config and --update
|
||||
Then the automation-profile add should succeed
|
||||
And the automation-profile output should contain "Updated"
|
||||
|
||||
Scenario: Add profile conflict without --update
|
||||
Given a valid automation profile config YAML file
|
||||
And the custom profile "acme/strict" already exists
|
||||
When I run automation-profile add with --config without --update
|
||||
Then the automation-profile command should abort
|
||||
And the automation-profile output should contain "Conflict"
|
||||
|
||||
Scenario: Add profile with missing config file fails
|
||||
When I run automation-profile add with --config pointing to a missing file
|
||||
Then the automation-profile command should abort
|
||||
|
||||
Scenario: Add profile with invalid YAML fails
|
||||
Given an invalid YAML automation profile config file
|
||||
When I run automation-profile add with --config pointing to the invalid YAML file
|
||||
Then the automation-profile command should abort
|
||||
|
||||
Scenario: Add profile with invalid name fails
|
||||
Given an automation profile config YAML file with invalid name
|
||||
When I run automation-profile add with --config pointing to the invalid name YAML file
|
||||
Then the automation-profile command should abort
|
||||
|
||||
Scenario: Add profile with invalid threshold fails
|
||||
Given an automation profile config YAML file with invalid threshold
|
||||
When I run automation-profile add with --config pointing to the invalid threshold YAML file
|
||||
Then the automation-profile command should abort
|
||||
|
||||
Scenario: Add profile cannot overwrite built-in
|
||||
Given an automation profile config YAML file with built-in name "manual"
|
||||
When I run automation-profile add with --config pointing to the built-in name YAML file
|
||||
Then the automation-profile command should abort
|
||||
And the automation-profile output should contain "built-in"
|
||||
|
||||
Scenario: Add profile with unsupported schema version
|
||||
Given an automation profile config YAML file with schema_version "2.0"
|
||||
When I run automation-profile add with --config pointing to the unsupported schema YAML file
|
||||
Then the automation-profile command should abort
|
||||
And the automation-profile output should contain "schema_version"
|
||||
|
||||
# List command tests
|
||||
Scenario: List profiles shows built-in profiles
|
||||
When I run automation-profile list
|
||||
Then the automation-profile list should succeed
|
||||
And the automation-profile output should contain "manual"
|
||||
And the automation-profile output should contain "auto"
|
||||
|
||||
Scenario: List profiles with namespace filter
|
||||
Given a custom profile "acme/strict" has been added
|
||||
When I run automation-profile list with namespace filter "acme"
|
||||
Then the automation-profile list should succeed
|
||||
And the automation-profile output should contain "acme/strict"
|
||||
|
||||
Scenario: List profiles with regex filter
|
||||
When I run automation-profile list with regex "caut.*"
|
||||
Then the automation-profile list should succeed
|
||||
And the automation-profile output should contain "cautious"
|
||||
|
||||
Scenario: List profiles with invalid regex
|
||||
When I run automation-profile list with regex "[invalid"
|
||||
Then the automation-profile command should abort
|
||||
|
||||
Scenario: List profiles with --format json output snapshot
|
||||
When I run automation-profile list with --format json
|
||||
Then the automation-profile list should succeed
|
||||
And the automation-profile json output should contain "name"
|
||||
And the automation-profile json output should contain "auto_strategize"
|
||||
|
||||
Scenario: List profiles with no matches
|
||||
When I run automation-profile list with namespace filter "nonexistent"
|
||||
Then the automation-profile output should contain "No profiles found"
|
||||
|
||||
# Show command tests
|
||||
Scenario: Show built-in profile details
|
||||
When I run automation-profile show "manual"
|
||||
Then the automation-profile show should succeed
|
||||
And the automation-profile output should contain "manual"
|
||||
And the automation-profile output should contain "1.0"
|
||||
|
||||
Scenario: Show profile not found
|
||||
When I run automation-profile show "nonexistent/profile"
|
||||
Then the automation-profile command should abort
|
||||
And the automation-profile output should contain "not found"
|
||||
|
||||
Scenario: Show profile with --format yaml output snapshot
|
||||
When I run automation-profile show "manual" with --format yaml
|
||||
Then the automation-profile show should succeed
|
||||
And the automation-profile yaml output should contain "name: manual"
|
||||
And the automation-profile yaml output should contain "auto_strategize"
|
||||
|
||||
Scenario: Show profile with --format json
|
||||
When I run automation-profile show "manual" with --format json
|
||||
Then the automation-profile show should succeed
|
||||
And the automation-profile json output should contain "name"
|
||||
|
||||
Scenario: Show profile with --format plain
|
||||
When I run automation-profile show "manual" with --format plain
|
||||
Then the automation-profile show should succeed
|
||||
And the automation-profile output should contain "name: manual"
|
||||
|
||||
Scenario: Show profile with --format table
|
||||
When I run automation-profile show "manual" with --format table
|
||||
Then the automation-profile show should succeed
|
||||
|
||||
# Remove command tests
|
||||
Scenario: Remove custom profile with confirmation
|
||||
Given a custom profile "acme/strict" has been added
|
||||
When I run automation-profile remove "acme/strict" with --yes
|
||||
Then the automation-profile remove should succeed
|
||||
And the automation-profile output should contain "removed"
|
||||
|
||||
Scenario: Remove built-in profile fails
|
||||
When I run automation-profile remove "manual" with --yes
|
||||
Then the automation-profile command should abort
|
||||
And the automation-profile output should contain "built-in"
|
||||
|
||||
Scenario: Remove nonexistent profile fails
|
||||
When I run automation-profile remove "nonexistent/profile" with --yes
|
||||
Then the automation-profile command should abort
|
||||
And the automation-profile output should contain "not found"
|
||||
|
||||
Scenario: Remove profile with --format json
|
||||
Given a custom profile "acme/toremove" has been added
|
||||
When I run automation-profile remove "acme/toremove" with --yes and --format json
|
||||
Then the automation-profile remove should succeed
|
||||
And the automation-profile json output should contain "removed"
|
||||
|
||||
# Deprecation warning tests
|
||||
Scenario: Deprecation warning for --automation-level on plan use
|
||||
When I invoke plan use with --automation-level "manual"
|
||||
Then the plan output should contain deprecation warning for automation-level
|
||||
@@ -0,0 +1,482 @@
|
||||
"""Step definitions for the Automation Profile CLI feature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
from behave.runner import Context
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from cleveragents.cli.commands.automation_profile import (
|
||||
_InMemoryProfileRepository,
|
||||
)
|
||||
from cleveragents.cli.commands.automation_profile import (
|
||||
app as profile_app,
|
||||
)
|
||||
from cleveragents.domain.models.core.automation_profile import AutomationProfile
|
||||
|
||||
_VALID_YAML = """\
|
||||
name: acme/strict
|
||||
description: Strict review for production
|
||||
schema_version: "1.0"
|
||||
auto_strategize: 1.0
|
||||
auto_execute: 1.0
|
||||
auto_apply: 1.0
|
||||
auto_decisions_strategize: 1.0
|
||||
auto_decisions_execute: 1.0
|
||||
auto_validation_fix: 1.0
|
||||
auto_strategy_revision: 1.0
|
||||
auto_reversion_from_apply: 1.0
|
||||
auto_child_plans: 1.0
|
||||
auto_retry_transient: 1.0
|
||||
auto_checkpoint_restore: 1.0
|
||||
require_sandbox: true
|
||||
require_checkpoints: true
|
||||
allow_unsafe_tools: false
|
||||
"""
|
||||
|
||||
_INVALID_NAME_YAML = """\
|
||||
name: "invalid name with spaces!"
|
||||
description: Bad profile
|
||||
schema_version: "1.0"
|
||||
"""
|
||||
|
||||
_INVALID_THRESHOLD_YAML = """\
|
||||
name: acme/bad-threshold
|
||||
description: Bad threshold
|
||||
schema_version: "1.0"
|
||||
auto_strategize: 2.0
|
||||
"""
|
||||
|
||||
_INVALID_YAML_CONTENT = """\
|
||||
name: [this is not valid yaml
|
||||
because: it has bad syntax
|
||||
"""
|
||||
|
||||
|
||||
def _make_custom_profile(
|
||||
name: str = "acme/strict",
|
||||
description: str = "Strict review for production",
|
||||
) -> AutomationProfile:
|
||||
"""Create a custom AutomationProfile for testing."""
|
||||
return AutomationProfile(
|
||||
name=name,
|
||||
description=description,
|
||||
schema_version="1.0",
|
||||
auto_strategize=1.0,
|
||||
auto_execute=1.0,
|
||||
auto_apply=1.0,
|
||||
auto_decisions_strategize=1.0,
|
||||
auto_decisions_execute=1.0,
|
||||
auto_validation_fix=1.0,
|
||||
auto_strategy_revision=1.0,
|
||||
auto_reversion_from_apply=1.0,
|
||||
auto_child_plans=1.0,
|
||||
auto_retry_transient=1.0,
|
||||
auto_checkpoint_restore=1.0,
|
||||
require_sandbox=True,
|
||||
require_checkpoints=True,
|
||||
allow_unsafe_tools=False,
|
||||
)
|
||||
|
||||
|
||||
def _write_temp_yaml(context: Context, content: str) -> str:
|
||||
"""Write YAML content to a temporary file and register cleanup."""
|
||||
fd, path = tempfile.mkstemp(suffix=".yaml")
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(content)
|
||||
if not hasattr(context, "_cleanup_handlers"):
|
||||
context._cleanup_handlers = []
|
||||
context._cleanup_handlers.append(
|
||||
lambda p=path: os.unlink(p) if os.path.exists(p) else None
|
||||
)
|
||||
return path
|
||||
|
||||
|
||||
@given("an automation profile CLI runner with mocks")
|
||||
def step_automation_profile_cli_runner(context: Context) -> None:
|
||||
"""Set up the CLI runner for testing."""
|
||||
context.runner = CliRunner()
|
||||
context.result = None
|
||||
# Reset the module-level in-memory repo for each scenario
|
||||
import cleveragents.cli.commands.automation_profile as ap_mod
|
||||
|
||||
ap_mod._repo = _InMemoryProfileRepository()
|
||||
|
||||
|
||||
@given("a valid automation profile config YAML file")
|
||||
def step_valid_profile_config(context: Context) -> None:
|
||||
"""Write a valid profile config to a temp file."""
|
||||
context.yaml_path = _write_temp_yaml(context, _VALID_YAML)
|
||||
|
||||
|
||||
@given('the custom profile "{name}" already exists')
|
||||
def step_custom_profile_exists(context: Context, name: str) -> None:
|
||||
"""Pre-register a custom profile in the in-memory repo."""
|
||||
import cleveragents.cli.commands.automation_profile as ap_mod
|
||||
|
||||
profile = _make_custom_profile(name=name)
|
||||
ap_mod._repo.upsert(profile)
|
||||
|
||||
|
||||
@given("an invalid YAML automation profile config file")
|
||||
def step_invalid_yaml_config(context: Context) -> None:
|
||||
"""Write an invalid YAML to a temp file."""
|
||||
context.invalid_yaml_path = _write_temp_yaml(context, _INVALID_YAML_CONTENT)
|
||||
|
||||
|
||||
@given("an automation profile config YAML file with invalid name")
|
||||
def step_invalid_name_config(context: Context) -> None:
|
||||
"""Write a profile config with invalid name to a temp file."""
|
||||
context.invalid_name_yaml_path = _write_temp_yaml(context, _INVALID_NAME_YAML)
|
||||
|
||||
|
||||
@given("an automation profile config YAML file with invalid threshold")
|
||||
def step_invalid_threshold_config(context: Context) -> None:
|
||||
"""Write a profile config with invalid threshold to a temp file."""
|
||||
context.invalid_threshold_yaml_path = _write_temp_yaml(
|
||||
context, _INVALID_THRESHOLD_YAML
|
||||
)
|
||||
|
||||
|
||||
@given('an automation profile config YAML file with built-in name "{name}"')
|
||||
def step_builtin_name_config(context: Context, name: str) -> None:
|
||||
"""Write a profile config with a built-in name to a temp file."""
|
||||
yaml_content = f"""\
|
||||
name: {name}
|
||||
description: Attempt to overwrite built-in
|
||||
schema_version: "1.0"
|
||||
auto_strategize: 0.5
|
||||
"""
|
||||
context.builtin_name_yaml_path = _write_temp_yaml(context, yaml_content)
|
||||
|
||||
|
||||
@given('an automation profile config YAML file with schema_version "{version}"')
|
||||
def step_unsupported_schema_config(context: Context, version: str) -> None:
|
||||
"""Write a profile config with unsupported schema version."""
|
||||
yaml_content = f"""\
|
||||
name: acme/unsupported
|
||||
description: Unsupported schema
|
||||
schema_version: "{version}"
|
||||
"""
|
||||
context.unsupported_schema_yaml_path = _write_temp_yaml(context, yaml_content)
|
||||
|
||||
|
||||
@given('a custom profile "{name}" has been added')
|
||||
def step_custom_profile_added(context: Context, name: str) -> None:
|
||||
"""Add a custom profile via the in-memory repo."""
|
||||
import cleveragents.cli.commands.automation_profile as ap_mod
|
||||
|
||||
profile = _make_custom_profile(name=name)
|
||||
ap_mod._repo.upsert(profile)
|
||||
|
||||
|
||||
# ---- When steps ---- #
|
||||
|
||||
|
||||
@when("I run automation-profile add with --config pointing to the YAML file")
|
||||
def step_run_add_config(context: Context) -> None:
|
||||
"""Run the add command with --config."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when("I run automation-profile add with --config and --update")
|
||||
def step_run_add_config_update(context: Context) -> None:
|
||||
"""Run the add command with --config and --update."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.yaml_path, "--update"]
|
||||
)
|
||||
|
||||
|
||||
@when("I run automation-profile add with --config without --update")
|
||||
def step_run_add_config_no_update(context: Context) -> None:
|
||||
"""Run the add command with --config but without --update."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when("I run automation-profile add with --config pointing to a missing file")
|
||||
def step_run_add_missing_file(context: Context) -> None:
|
||||
"""Run the add command with a missing config file."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", "/nonexistent/path/profile.yaml"]
|
||||
)
|
||||
|
||||
|
||||
@when("I run automation-profile add with --config pointing to the invalid YAML file")
|
||||
def step_run_add_invalid_yaml(context: Context) -> None:
|
||||
"""Run the add command with invalid YAML."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.invalid_yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
"I run automation-profile add with --config pointing to the invalid name YAML file"
|
||||
)
|
||||
def step_run_add_invalid_name(context: Context) -> None:
|
||||
"""Run the add command with invalid name."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.invalid_name_yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
"I run automation-profile add with --config pointing to the invalid threshold "
|
||||
"YAML file"
|
||||
)
|
||||
def step_run_add_invalid_threshold(context: Context) -> None:
|
||||
"""Run the add command with invalid threshold."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.invalid_threshold_yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
"I run automation-profile add with --config pointing to the built-in name YAML file"
|
||||
)
|
||||
def step_run_add_builtin_name(context: Context) -> None:
|
||||
"""Run the add command with a built-in name."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.builtin_name_yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
"I run automation-profile add with --config pointing to the unsupported "
|
||||
"schema YAML file"
|
||||
)
|
||||
def step_run_add_unsupported_schema(context: Context) -> None:
|
||||
"""Run the add command with unsupported schema version."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["add", "--config", context.unsupported_schema_yaml_path]
|
||||
)
|
||||
|
||||
|
||||
@when("I run automation-profile list")
|
||||
def step_run_list(context: Context) -> None:
|
||||
"""Run the list command."""
|
||||
context.result = context.runner.invoke(profile_app, ["list"])
|
||||
|
||||
|
||||
@when('I run automation-profile list with namespace filter "{namespace}"')
|
||||
def step_run_list_namespace(context: Context, namespace: str) -> None:
|
||||
"""Run the list command with a namespace filter."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["list", "--namespace", namespace]
|
||||
)
|
||||
|
||||
|
||||
@when('I run automation-profile list with regex "{regex}"')
|
||||
def step_run_list_regex(context: Context, regex: str) -> None:
|
||||
"""Run the list command with a regex filter."""
|
||||
context.result = context.runner.invoke(profile_app, ["list", regex])
|
||||
|
||||
|
||||
@when("I run automation-profile list with --format json")
|
||||
def step_run_list_format_json(context: Context) -> None:
|
||||
"""Run the list command with --format json."""
|
||||
context.result = context.runner.invoke(profile_app, ["list", "--format", "json"])
|
||||
|
||||
|
||||
@when('I run automation-profile show "{name}"')
|
||||
def step_run_show(context: Context, name: str) -> None:
|
||||
"""Run the show command."""
|
||||
context.result = context.runner.invoke(profile_app, ["show", name])
|
||||
|
||||
|
||||
@when('I run automation-profile show "{name}" with --format yaml')
|
||||
def step_run_show_format_yaml(context: Context, name: str) -> None:
|
||||
"""Run the show command with --format yaml."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["show", name, "--format", "yaml"]
|
||||
)
|
||||
|
||||
|
||||
@when('I run automation-profile show "{name}" with --format json')
|
||||
def step_run_show_format_json(context: Context, name: str) -> None:
|
||||
"""Run the show command with --format json."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["show", name, "--format", "json"]
|
||||
)
|
||||
|
||||
|
||||
@when('I run automation-profile show "{name}" with --format plain')
|
||||
def step_run_show_format_plain(context: Context, name: str) -> None:
|
||||
"""Run the show command with --format plain."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["show", name, "--format", "plain"]
|
||||
)
|
||||
|
||||
|
||||
@when('I run automation-profile show "{name}" with --format table')
|
||||
def step_run_show_format_table(context: Context, name: str) -> None:
|
||||
"""Run the show command with --format table."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["show", name, "--format", "table"]
|
||||
)
|
||||
|
||||
|
||||
@when('I run automation-profile remove "{name}" with --yes')
|
||||
def step_run_remove_yes(context: Context, name: str) -> None:
|
||||
"""Run the remove command with --yes."""
|
||||
context.result = context.runner.invoke(profile_app, ["remove", name, "--yes"])
|
||||
|
||||
|
||||
@when('I run automation-profile remove "{name}" with --yes and --format json')
|
||||
def step_run_remove_yes_json(context: Context, name: str) -> None:
|
||||
"""Run the remove command with --yes and --format json."""
|
||||
context.result = context.runner.invoke(
|
||||
profile_app, ["remove", name, "--yes", "--format", "json"]
|
||||
)
|
||||
|
||||
|
||||
@when('I invoke plan use with --automation-level "{level}"')
|
||||
def step_invoke_plan_use_automation_level(context: Context, level: str) -> None:
|
||||
"""Invoke plan use with the deprecated --automation-level flag."""
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
|
||||
# Mock the lifecycle service so we don't need real infra
|
||||
mock_service = MagicMock()
|
||||
mock_plan = MagicMock()
|
||||
mock_plan.identity.plan_id = "test-plan-id"
|
||||
mock_plan.namespaced_name = "test/plan"
|
||||
mock_plan.phase.value = "strategize"
|
||||
mock_plan.processing_state.value = "queued"
|
||||
mock_plan.state.value = "queued"
|
||||
mock_plan.project_links = []
|
||||
mock_plan.arguments = {}
|
||||
mock_plan.automation_profile = None
|
||||
mock_plan.action_name = "test/action"
|
||||
mock_plan.description = "test"
|
||||
mock_plan.definition_of_done = "test"
|
||||
mock_plan.automation_level.value = "manual"
|
||||
mock_plan.strategy_actor = "test"
|
||||
mock_plan.execution_actor = "test"
|
||||
mock_plan.estimation_actor = None
|
||||
mock_plan.invariant_actor = None
|
||||
mock_plan.arguments_order = None
|
||||
mock_plan.timestamps.created_at.isoformat.return_value = "2024-01-01T00:00:00"
|
||||
mock_plan.timestamps.updated_at.isoformat.return_value = "2024-01-01T00:00:00"
|
||||
mock_plan.timestamps.strategize_started_at = None
|
||||
mock_plan.timestamps.strategize_completed_at = None
|
||||
mock_plan.timestamps.execute_started_at = None
|
||||
mock_plan.timestamps.execute_completed_at = None
|
||||
mock_plan.timestamps.applied_at = None
|
||||
mock_plan.error_message = None
|
||||
mock_plan.is_terminal = False
|
||||
mock_service.get_action_by_name.return_value = MagicMock()
|
||||
mock_service.get_action_by_name.return_value.namespaced_name = "test/action"
|
||||
mock_service.use_action.return_value = mock_plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
context.result = context.runner.invoke(
|
||||
plan_app,
|
||||
[
|
||||
"use",
|
||||
"test/action",
|
||||
"--automation-level",
|
||||
level,
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
# ---- Then steps ---- #
|
||||
|
||||
|
||||
@then("the automation-profile add should succeed")
|
||||
def step_add_should_succeed(context: Context) -> None:
|
||||
"""Assert the add command succeeded."""
|
||||
assert context.result is not None
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit code 0, got {context.result.exit_code}. "
|
||||
f"Output: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then('the automation-profile output should contain "{text}"')
|
||||
def step_output_contains(context: Context, text: str) -> None:
|
||||
"""Assert the command output contains specific text."""
|
||||
assert context.result is not None
|
||||
assert text.lower() in context.result.output.lower(), (
|
||||
f"Expected '{text}' in output. Got: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the automation-profile command should abort")
|
||||
def step_command_should_abort(context: Context) -> None:
|
||||
"""Assert the command aborted with non-zero exit code."""
|
||||
assert context.result is not None
|
||||
assert context.result.exit_code != 0, (
|
||||
f"Expected non-zero exit code, got {context.result.exit_code}. "
|
||||
f"Output: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the automation-profile list should succeed")
|
||||
def step_list_should_succeed(context: Context) -> None:
|
||||
"""Assert the list command succeeded."""
|
||||
assert context.result is not None
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit code 0, got {context.result.exit_code}. "
|
||||
f"Output: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then('the automation-profile json output should contain "{text}"')
|
||||
def step_json_output_contains(context: Context, text: str) -> None:
|
||||
"""Assert the JSON output contains specific text."""
|
||||
assert context.result is not None
|
||||
assert text in context.result.output, (
|
||||
f"Expected '{text}' in JSON output. Got: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then('the automation-profile yaml output should contain "{text}"')
|
||||
def step_yaml_output_contains(context: Context, text: str) -> None:
|
||||
"""Assert the YAML output contains specific text."""
|
||||
assert context.result is not None
|
||||
assert text in context.result.output, (
|
||||
f"Expected '{text}' in YAML output. Got: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the automation-profile show should succeed")
|
||||
def step_show_should_succeed(context: Context) -> None:
|
||||
"""Assert the show command succeeded."""
|
||||
assert context.result is not None
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit code 0, got {context.result.exit_code}. "
|
||||
f"Output: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the automation-profile remove should succeed")
|
||||
def step_remove_should_succeed(context: Context) -> None:
|
||||
"""Assert the remove command succeeded."""
|
||||
assert context.result is not None
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Expected exit code 0, got {context.result.exit_code}. "
|
||||
f"Output: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the plan output should contain deprecation warning for automation-level")
|
||||
def step_plan_output_deprecation_warning(context: Context) -> None:
|
||||
"""Assert the plan output contains deprecation warning."""
|
||||
assert context.result is not None
|
||||
output = context.result.output.lower()
|
||||
assert "deprecated" in output, (
|
||||
f"Expected 'deprecated' in output. Got: {context.result.output}"
|
||||
)
|
||||
+21
-21
@@ -2094,27 +2094,27 @@ No standalone Q0-Advanced commits planned. Advanced QA enhancements are bundled
|
||||
- [X] Git [Jeff]: `git branch -d feature/m4-automation-profiles-service`
|
||||
- [X] Quality [Jeff]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
|
||||
- [ ] **COMMIT (Owner: Jeff | Group: A6.cli | Branch: feature/m4-automation-profiles-cli | Planned: Day 24 | Expected: Day 25) - Commit message: "feat(cli): add automation-profile commands"**
|
||||
- [ ] Git [Jeff]: `git checkout master`
|
||||
- [ ] Git [Jeff]: `git pull origin master`
|
||||
- [ ] Git [Jeff]: `git checkout -b feature/m4-automation-profiles-cli`
|
||||
- [ ] Git [Jeff]: `git fetch origin && git merge origin/master`
|
||||
- [ ] Code [Jeff]: Implement `agents automation-profile add/remove/list/show` commands with YAML config input, schema_version guard, and namespaced name validation.
|
||||
- [ ] Code [Jeff]: Add `--update` behavior with conflict errors; preserve original created_at on update, and surface `source` (built-in/custom) in outputs.
|
||||
- [ ] Code [Jeff]: Ensure `automation-profile list` supports `--namespace` and regex filters with deterministic ordering; add `--format json|yaml|plain` output.
|
||||
- [ ] Code [Jeff]: Extend `plan use` with `--automation-profile` and show profile name + threshold summary in `plan status` output.
|
||||
- [ ] Code [Jeff]: Deprecate `--automation-level` and `plan set-automation-level` (keep as alias mapping to built-in profiles with warning).
|
||||
- [ ] Docs [Jeff]: Update CLI reference with automation-profile examples, built-in profiles list, and deprecation notes.
|
||||
- [ ] Tests (Behave) [Jeff]: Add CLI scenarios for profile add/list/show/remove/update and invalid name/threshold validation errors.
|
||||
- [ ] Tests (Behave) [Jeff]: Add output snapshot assertions for `automation-profile list --format json` and `show --format yaml`.
|
||||
- [ ] Tests (Robot) [Jeff]: Add Robot CLI tests for automation-profile commands (add/show/remove + format outputs).
|
||||
- [ ] Tests (ASV) [Jeff]: Add `benchmarks/automation_profile_cli_bench.py` for CLI parsing.
|
||||
- [ ] Quality [Jeff]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
- [ ] Quality [Jeff]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
|
||||
- [ ] Git [Jeff]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
|
||||
- [ ] Git [Jeff]: `git commit -m "feat(cli): add automation-profile commands"`
|
||||
- [ ] Git [Jeff]: `git push -u origin feature/m4-automation-profiles-cli`
|
||||
- [ ] Forgejo PR [Jeff]: Open PR from `feature/m4-automation-profiles-cli` to `master` with description "Add automation-profile CLI commands, output formats, and tests.".
|
||||
- [X] **COMMIT (Owner: Jeff | Group: A6.cli | Branch: feature/m4-automation-profiles-cli | Planned: Day 24 | Expected: Day 25) - Commit message: "feat(cli): add automation-profile commands"**
|
||||
- [X] Git [Jeff]: `git checkout master`
|
||||
- [X] Git [Jeff]: `git pull origin master`
|
||||
- [X] Git [Jeff]: `git checkout -b feature/m4-automation-profiles-cli`
|
||||
- [X] Git [Jeff]: `git fetch origin && git merge origin/master`
|
||||
- [X] Code [Jeff]: Implement `agents automation-profile add/remove/list/show` commands with YAML config input, schema_version guard, and namespaced name validation.
|
||||
- [X] Code [Jeff]: Add `--update` behavior with conflict errors; preserve original created_at on update, and surface `source` (built-in/custom) in outputs.
|
||||
- [X] Code [Jeff]: Ensure `automation-profile list` supports `--namespace` and regex filters with deterministic ordering; add `--format json|yaml|plain` output.
|
||||
- [X] Code [Jeff]: Extend `plan use` with `--automation-profile` and show profile name + threshold summary in `plan status` output.
|
||||
- [X] Code [Jeff]: Deprecate `--automation-level` and `plan set-automation-level` (keep as alias mapping to built-in profiles with warning).
|
||||
- [X] Docs [Jeff]: Update CLI reference with automation-profile examples, built-in profiles list, and deprecation notes.
|
||||
- [X] Tests (Behave) [Jeff]: Add CLI scenarios for profile add/list/show/remove/update and invalid name/threshold validation errors.
|
||||
- [X] Tests (Behave) [Jeff]: Add output snapshot assertions for `automation-profile list --format json` and `show --format yaml`.
|
||||
- [X] Tests (Robot) [Jeff]: Add Robot CLI tests for automation-profile commands (add/show/remove + format outputs).
|
||||
- [X] Tests (ASV) [Jeff]: Add `benchmarks/automation_profile_cli_bench.py` for CLI parsing.
|
||||
- [X] Quality [Jeff]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
- [X] Quality [Jeff]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
|
||||
- [X] Git [Jeff]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
|
||||
- [X] Git [Jeff]: `git commit -m "feat(cli): add automation-profile commands"`
|
||||
- [X] Git [Jeff]: `git push -u origin feature/m4-automation-profiles-cli`
|
||||
- [X] Forgejo PR [Jeff]: Open PR from `feature/m4-automation-profiles-cli` to `master` with description "Add automation-profile CLI commands, output formats, and tests.".
|
||||
|
||||
- [ ] **COMMIT (Owner: Jeff | Group: A6.legacy | Branch: feature/m4-automation-legacy-cleanup | Planned: Day 25 | Expected: Day 26) - Commit message: "refactor(automation): remove automation_level legacy fields"**
|
||||
- [ ] Git [Jeff]: `git checkout master`
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
*** Settings ***
|
||||
Documentation Smoke tests for automation-profile CLI commands
|
||||
Resource ${CURDIR}/common.resource
|
||||
Suite Setup Setup Test Environment
|
||||
Suite Teardown Cleanup Test Environment
|
||||
|
||||
*** Variables ***
|
||||
${HELPER_SCRIPT} robot/helper_automation_profile_cli.py
|
||||
|
||||
*** Test Cases ***
|
||||
Add Automation Profile
|
||||
[Documentation] Add a custom automation profile from YAML config
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} add cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} add-profile-ok
|
||||
|
||||
Show Automation Profile
|
||||
[Documentation] Show a built-in automation profile
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} show cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} show-profile-ok
|
||||
|
||||
Show Automation Profile JSON
|
||||
[Documentation] Show a profile in JSON format
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} show-json cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} show-json-ok
|
||||
|
||||
Show Automation Profile YAML
|
||||
[Documentation] Show a profile in YAML format
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} show-yaml cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} show-yaml-ok
|
||||
|
||||
List Automation Profiles
|
||||
[Documentation] List all automation profiles
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} list cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} list-profiles-ok
|
||||
|
||||
List Automation Profiles JSON
|
||||
[Documentation] List profiles in JSON format
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} list-json cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} list-json-ok
|
||||
|
||||
Remove Automation Profile
|
||||
[Documentation] Remove a custom automation profile
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} remove cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} remove-profile-ok
|
||||
|
||||
Remove Built-in Profile Fails
|
||||
[Documentation] Verify built-in profiles cannot be removed
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} remove-builtin-fails cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} remove-builtin-fails-ok
|
||||
|
||||
All Automation Profile CLI Tests
|
||||
[Documentation] Run all automation-profile CLI tests
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} all cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} all-tests-ok
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Robot Framework helper for automation-profile CLI smoke tests.
|
||||
|
||||
Invoked by Robot tests to exercise the automation-profile CLI commands
|
||||
in isolation using the Typer CliRunner with mocked services.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure the local source tree is importable
|
||||
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from typer.testing import CliRunner # noqa: E402
|
||||
|
||||
import cleveragents.cli.commands.automation_profile as _ap_mod # noqa: E402
|
||||
|
||||
_InMemoryProfileRepository = _ap_mod._InMemoryProfileRepository
|
||||
profile_app = _ap_mod.app
|
||||
|
||||
_runner = CliRunner()
|
||||
|
||||
_VALID_YAML = """\
|
||||
name: acme/robot-test
|
||||
description: Robot test profile
|
||||
schema_version: "1.0"
|
||||
auto_strategize: 0.8
|
||||
auto_execute: 0.7
|
||||
auto_apply: 1.0
|
||||
auto_decisions_strategize: 0.6
|
||||
auto_decisions_execute: 0.8
|
||||
auto_validation_fix: 0.7
|
||||
auto_strategy_revision: 0.8
|
||||
auto_reversion_from_apply: 0.9
|
||||
auto_child_plans: 0.7
|
||||
auto_retry_transient: 0.0
|
||||
auto_checkpoint_restore: 0.6
|
||||
require_sandbox: true
|
||||
require_checkpoints: true
|
||||
allow_unsafe_tools: false
|
||||
"""
|
||||
|
||||
|
||||
def _reset_repo() -> None:
|
||||
"""Reset the module-level in-memory repo."""
|
||||
import cleveragents.cli.commands.automation_profile as ap_mod
|
||||
|
||||
ap_mod._repo = _InMemoryProfileRepository()
|
||||
|
||||
|
||||
def test_add_profile() -> None:
|
||||
"""Test adding a profile via YAML config."""
|
||||
_reset_repo()
|
||||
fd, path = tempfile.mkstemp(suffix=".yaml")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(_VALID_YAML)
|
||||
result = _runner.invoke(profile_app, ["add", "--config", path])
|
||||
assert result.exit_code == 0, f"add failed: {result.output}"
|
||||
print("add-profile-ok")
|
||||
finally:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def test_show_profile() -> None:
|
||||
"""Test showing a built-in profile."""
|
||||
_reset_repo()
|
||||
result = _runner.invoke(profile_app, ["show", "manual"])
|
||||
assert result.exit_code == 0, f"show failed: {result.output}"
|
||||
assert "manual" in result.output
|
||||
print("show-profile-ok")
|
||||
|
||||
|
||||
def test_show_json() -> None:
|
||||
"""Test showing a profile in JSON format."""
|
||||
_reset_repo()
|
||||
result = _runner.invoke(profile_app, ["show", "manual", "--format", "json"])
|
||||
assert result.exit_code == 0, f"show json failed: {result.output}"
|
||||
# Verify it's valid JSON
|
||||
parsed = json.loads(result.output)
|
||||
assert parsed["name"] == "manual"
|
||||
print("show-json-ok")
|
||||
|
||||
|
||||
def test_show_yaml() -> None:
|
||||
"""Test showing a profile in YAML format."""
|
||||
_reset_repo()
|
||||
result = _runner.invoke(profile_app, ["show", "manual", "--format", "yaml"])
|
||||
assert result.exit_code == 0, f"show yaml failed: {result.output}"
|
||||
assert "name: manual" in result.output
|
||||
print("show-yaml-ok")
|
||||
|
||||
|
||||
def test_list_profiles() -> None:
|
||||
"""Test listing all profiles."""
|
||||
_reset_repo()
|
||||
result = _runner.invoke(profile_app, ["list"])
|
||||
assert result.exit_code == 0, f"list failed: {result.output}"
|
||||
assert "manual" in result.output
|
||||
print("list-profiles-ok")
|
||||
|
||||
|
||||
def test_list_json() -> None:
|
||||
"""Test listing profiles in JSON format."""
|
||||
_reset_repo()
|
||||
result = _runner.invoke(profile_app, ["list", "--format", "json"])
|
||||
assert result.exit_code == 0, f"list json failed: {result.output}"
|
||||
parsed = json.loads(result.output)
|
||||
assert isinstance(parsed, list)
|
||||
assert len(parsed) >= 8 # At least 8 built-in profiles
|
||||
print("list-json-ok")
|
||||
|
||||
|
||||
def test_remove_profile() -> None:
|
||||
"""Test removing a custom profile."""
|
||||
_reset_repo()
|
||||
# First add a profile
|
||||
fd, path = tempfile.mkstemp(suffix=".yaml")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as fh:
|
||||
fh.write(_VALID_YAML)
|
||||
add_result = _runner.invoke(profile_app, ["add", "--config", path])
|
||||
assert add_result.exit_code == 0, f"add failed: {add_result.output}"
|
||||
finally:
|
||||
Path(path).unlink(missing_ok=True)
|
||||
|
||||
# Then remove it
|
||||
result = _runner.invoke(profile_app, ["remove", "acme/robot-test", "--yes"])
|
||||
assert result.exit_code == 0, f"remove failed: {result.output}"
|
||||
assert "removed" in result.output.lower()
|
||||
print("remove-profile-ok")
|
||||
|
||||
|
||||
def test_remove_builtin_fails() -> None:
|
||||
"""Test that removing a built-in profile fails."""
|
||||
_reset_repo()
|
||||
result = _runner.invoke(profile_app, ["remove", "manual", "--yes"])
|
||||
assert result.exit_code != 0, f"remove should have failed: {result.output}"
|
||||
print("remove-builtin-fails-ok")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
command = sys.argv[1] if len(sys.argv) > 1 else "all"
|
||||
|
||||
tests = {
|
||||
"add": test_add_profile,
|
||||
"show": test_show_profile,
|
||||
"show-json": test_show_json,
|
||||
"show-yaml": test_show_yaml,
|
||||
"list": test_list_profiles,
|
||||
"list-json": test_list_json,
|
||||
"remove": test_remove_profile,
|
||||
"remove-builtin-fails": test_remove_builtin_fails,
|
||||
}
|
||||
|
||||
if command == "all":
|
||||
for _name, test_fn in tests.items():
|
||||
test_fn()
|
||||
print("all-tests-ok")
|
||||
elif command in tests:
|
||||
tests[command]()
|
||||
else:
|
||||
print(f"Unknown test: {command}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,547 @@
|
||||
"""Automation profile management commands for CleverAgents CLI.
|
||||
|
||||
The ``agents automation-profile`` command group manages automation profiles
|
||||
that control how much autonomy the system has during plan execution.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------------------------------------|------------------------------------|
|
||||
| ``agents automation-profile add`` | Add/register a profile from YAML |
|
||||
| ``agents automation-profile remove`` | Remove a custom profile by name |
|
||||
| ``agents automation-profile list`` | List profiles with optional filters|
|
||||
| ``agents automation-profile show`` | Show full profile details |
|
||||
|
||||
## YAML Configuration File
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
name: acme/strict
|
||||
description: Strict review for production
|
||||
schema_version: "1.0"
|
||||
auto_strategize: 1.0
|
||||
auto_execute: 1.0
|
||||
auto_apply: 1.0
|
||||
...
|
||||
|
||||
## Profile Naming
|
||||
|
||||
Built-in profiles use bare names (e.g. ``manual``). Custom profiles
|
||||
use a ``namespace/name`` pattern (e.g. ``acme/strict``).
|
||||
|
||||
Based on v3_spec.md implementation plan Stage A6.cli.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import re
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
import yaml
|
||||
from pydantic import ValidationError as PydanticValidationError
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from cleveragents.application.services.automation_profile_service import (
|
||||
AutomationProfileService,
|
||||
)
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.core.exceptions import (
|
||||
CleverAgentsError,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
from cleveragents.domain.models.core.automation_profile import (
|
||||
BUILTIN_PROFILES,
|
||||
AutomationProfile,
|
||||
)
|
||||
|
||||
# Create sub-app for automation-profile commands
|
||||
app = typer.Typer(
|
||||
help="Manage automation profiles that control plan execution autonomy."
|
||||
)
|
||||
console = Console()
|
||||
|
||||
# Reusable --format option description
|
||||
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
|
||||
|
||||
|
||||
class _InMemoryProfileRepository:
|
||||
"""Minimal in-memory repository for custom automation profiles.
|
||||
|
||||
Implements the same interface as ``AutomationProfileRepository`` so
|
||||
``AutomationProfileService`` can be used without a database. This
|
||||
will be replaced by the DB-backed repository once full persistence
|
||||
is wired into the DI container.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._profiles: dict[str, AutomationProfile] = {}
|
||||
|
||||
def get_by_name(self, name: str) -> AutomationProfile | None:
|
||||
"""Look up a profile by name."""
|
||||
return self._profiles.get(name)
|
||||
|
||||
def list_all(self) -> list[AutomationProfile]:
|
||||
"""Return all persisted profiles ordered by name."""
|
||||
return [self._profiles[k] for k in sorted(self._profiles)]
|
||||
|
||||
def upsert(
|
||||
self,
|
||||
profile: AutomationProfile,
|
||||
expected_schema_version: str | None = None,
|
||||
) -> None:
|
||||
"""Insert or update a profile."""
|
||||
self._profiles[profile.name] = profile
|
||||
|
||||
def delete(self, name: str) -> None:
|
||||
"""Delete a profile by name.
|
||||
|
||||
Raises ``NotFoundError`` if not present.
|
||||
"""
|
||||
if name not in self._profiles:
|
||||
raise NotFoundError(
|
||||
f"Profile '{name}' not found",
|
||||
resource_type="automation_profile",
|
||||
resource_id=name,
|
||||
)
|
||||
del self._profiles[name]
|
||||
|
||||
|
||||
# Module-level in-memory repo so profiles persist within a CLI session
|
||||
_repo = _InMemoryProfileRepository()
|
||||
|
||||
|
||||
def _get_service() -> AutomationProfileService:
|
||||
"""Get the AutomationProfileService with in-memory repository.
|
||||
|
||||
Uses a module-level in-memory repository. When the full DI
|
||||
container is wired this helper will be updated to use the
|
||||
container instead.
|
||||
|
||||
The in-memory repo implements the same interface as
|
||||
``AutomationProfileRepository`` and is duck-type compatible.
|
||||
"""
|
||||
# The in-memory repo is duck-type compatible with AutomationProfileRepository.
|
||||
# Using Any cast for the type checker since the formal type is only
|
||||
# available behind TYPE_CHECKING (to avoid importing SQLAlchemy).
|
||||
repo: Any = _repo
|
||||
return AutomationProfileService(repo=repo)
|
||||
|
||||
|
||||
def _profile_spec_dict(profile: AutomationProfile) -> dict[str, object]:
|
||||
"""Return profile data as a dict suitable for CLI output.
|
||||
|
||||
Keys match the AutomationProfile field names for consistency
|
||||
with the YAML config format.
|
||||
"""
|
||||
source = "built-in" if profile.name in BUILTIN_PROFILES else "custom"
|
||||
result: dict[str, object] = {
|
||||
"name": profile.name,
|
||||
"description": profile.description,
|
||||
"source": source,
|
||||
"schema_version": profile.schema_version,
|
||||
"auto_strategize": profile.auto_strategize,
|
||||
"auto_execute": profile.auto_execute,
|
||||
"auto_apply": profile.auto_apply,
|
||||
"auto_decisions_strategize": profile.auto_decisions_strategize,
|
||||
"auto_decisions_execute": profile.auto_decisions_execute,
|
||||
"auto_validation_fix": profile.auto_validation_fix,
|
||||
"auto_strategy_revision": profile.auto_strategy_revision,
|
||||
"auto_reversion_from_apply": profile.auto_reversion_from_apply,
|
||||
"auto_child_plans": profile.auto_child_plans,
|
||||
"auto_retry_transient": profile.auto_retry_transient,
|
||||
"auto_checkpoint_restore": profile.auto_checkpoint_restore,
|
||||
"require_sandbox": profile.require_sandbox,
|
||||
"require_checkpoints": profile.require_checkpoints,
|
||||
"allow_unsafe_tools": profile.allow_unsafe_tools,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _threshold_summary(profile: AutomationProfile) -> str:
|
||||
"""Return a compact summary of key thresholds."""
|
||||
return (
|
||||
f"strategize={profile.auto_strategize:.1f} "
|
||||
f"execute={profile.auto_execute:.1f} "
|
||||
f"apply={profile.auto_apply:.1f}"
|
||||
)
|
||||
|
||||
|
||||
def _print_profile(
|
||||
profile: AutomationProfile,
|
||||
title: str = "Automation Profile",
|
||||
fmt: str = OutputFormat.RICH.value,
|
||||
) -> None:
|
||||
"""Print profile details in the requested format."""
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _profile_spec_dict(profile)
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
source = "built-in" if profile.name in BUILTIN_PROFILES else "custom"
|
||||
|
||||
details = (
|
||||
f"[bold]Name:[/bold] {profile.name}\n"
|
||||
f"[bold]Description:[/bold] {profile.description}\n"
|
||||
f"[bold]Source:[/bold] {source}\n"
|
||||
f"[bold]Schema Version:[/bold] {profile.schema_version}\n"
|
||||
f"\n[bold]Phase-Transition Thresholds:[/bold]\n"
|
||||
f" auto_strategize: {profile.auto_strategize}\n"
|
||||
f" auto_execute: {profile.auto_execute}\n"
|
||||
f" auto_apply: {profile.auto_apply}\n"
|
||||
f"\n[bold]Decision-Autonomy Thresholds:[/bold]\n"
|
||||
f" auto_decisions_strategize: {profile.auto_decisions_strategize}\n"
|
||||
f" auto_decisions_execute: {profile.auto_decisions_execute}\n"
|
||||
f"\n[bold]Self-Repair Thresholds:[/bold]\n"
|
||||
f" auto_validation_fix: {profile.auto_validation_fix}\n"
|
||||
f" auto_strategy_revision: {profile.auto_strategy_revision}\n"
|
||||
f" auto_reversion_from_apply: {profile.auto_reversion_from_apply}\n"
|
||||
f"\n[bold]Child Plan & Retry Thresholds:[/bold]\n"
|
||||
f" auto_child_plans: {profile.auto_child_plans}\n"
|
||||
f" auto_retry_transient: {profile.auto_retry_transient}\n"
|
||||
f" auto_checkpoint_restore: {profile.auto_checkpoint_restore}\n"
|
||||
f"\n[bold]Safety Requirements:[/bold]\n"
|
||||
f" require_sandbox: {profile.require_sandbox}\n"
|
||||
f" require_checkpoints: {profile.require_checkpoints}\n"
|
||||
f" allow_unsafe_tools: {profile.allow_unsafe_tools}"
|
||||
)
|
||||
|
||||
console.print(Panel(details, title=title, expand=False))
|
||||
|
||||
|
||||
@app.command("add")
|
||||
def add_profile(
|
||||
config: Annotated[
|
||||
Path,
|
||||
typer.Option(
|
||||
"--config",
|
||||
"-c",
|
||||
help="Path to the automation profile YAML configuration file",
|
||||
exists=False,
|
||||
),
|
||||
],
|
||||
update: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--update",
|
||||
help="Update an existing profile instead of creating a new one",
|
||||
),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Add or register an automation profile from a YAML configuration file.
|
||||
|
||||
Profiles are created via ``--config <file>``. Use ``--update`` to
|
||||
overwrite an existing custom profile.
|
||||
|
||||
Examples:
|
||||
agents automation-profile add --config ./profiles/strict.yaml
|
||||
agents automation-profile add --config ./profiles/strict.yaml --update
|
||||
"""
|
||||
try:
|
||||
if not config.exists():
|
||||
console.print(f"[red]Config file error:[/red] File not found: {config}")
|
||||
raise typer.Abort()
|
||||
|
||||
with open(config) as fh:
|
||||
config_data: dict[str, Any] = yaml.safe_load(fh)
|
||||
|
||||
if not isinstance(config_data, dict):
|
||||
console.print(
|
||||
"[red]Config file error:[/red] YAML file must contain a mapping (dict)"
|
||||
)
|
||||
raise typer.Abort()
|
||||
|
||||
# Validate schema_version is present and supported
|
||||
schema_version = config_data.get("schema_version", "1.0")
|
||||
if schema_version not in ("1.0",):
|
||||
console.print(
|
||||
f"[red]Schema validation error:[/red] "
|
||||
f"Unsupported schema_version '{schema_version}'. "
|
||||
f"Supported versions: 1.0"
|
||||
)
|
||||
raise typer.Abort()
|
||||
|
||||
service = _get_service()
|
||||
|
||||
name = config_data.get("name", "")
|
||||
existing = None
|
||||
with contextlib.suppress(NotFoundError, CleverAgentsError):
|
||||
existing = service.get_profile(name)
|
||||
|
||||
if existing and not update:
|
||||
if name in BUILTIN_PROFILES:
|
||||
console.print(
|
||||
f"[red]Conflict:[/red] Cannot overwrite built-in profile '{name}'"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"[red]Conflict:[/red] "
|
||||
f"Profile '{name}' already exists. "
|
||||
f"Use --update to overwrite."
|
||||
)
|
||||
raise typer.Abort()
|
||||
|
||||
if update and existing:
|
||||
profile = service.update_profile(name, config_data)
|
||||
title = "Profile Updated"
|
||||
else:
|
||||
profile = service.create_profile(config_data)
|
||||
title = "Profile Added"
|
||||
|
||||
_print_profile(profile, title=title, fmt=fmt)
|
||||
|
||||
except FileNotFoundError as exc:
|
||||
console.print(f"[red]Config file error:[/red] {exc}")
|
||||
raise typer.Abort() from exc
|
||||
except PydanticValidationError as exc:
|
||||
console.print(f"[red]Schema validation error:[/red] {exc}")
|
||||
raise typer.Abort() from exc
|
||||
except yaml.YAMLError as exc:
|
||||
console.print(f"[red]YAML parse error:[/red] {exc}")
|
||||
raise typer.Abort() from exc
|
||||
except ValidationError as exc:
|
||||
console.print(f"[red]Validation Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
except CleverAgentsError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
|
||||
|
||||
@app.command("remove")
|
||||
def remove_profile(
|
||||
name: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Name of the automation profile to remove"),
|
||||
],
|
||||
yes: Annotated[
|
||||
bool,
|
||||
typer.Option(
|
||||
"--yes",
|
||||
"-y",
|
||||
help="Skip confirmation prompt",
|
||||
),
|
||||
] = False,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Remove a custom automation profile by name.
|
||||
|
||||
Built-in profiles cannot be removed.
|
||||
|
||||
Examples:
|
||||
agents automation-profile remove acme/strict
|
||||
agents automation-profile remove acme/strict --yes
|
||||
"""
|
||||
try:
|
||||
service = _get_service()
|
||||
|
||||
# Verify profile exists
|
||||
profile = service.get_profile(name)
|
||||
|
||||
if name in BUILTIN_PROFILES:
|
||||
console.print(f"[red]Cannot remove built-in profile:[/red] '{name}'")
|
||||
raise typer.Abort()
|
||||
|
||||
if not yes:
|
||||
confirm = typer.confirm(f"Remove automation profile '{name}'?")
|
||||
if not confirm:
|
||||
console.print("[yellow]Cancelled.[/yellow]")
|
||||
raise typer.Exit(0)
|
||||
|
||||
service.delete_profile(name)
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _profile_spec_dict(profile)
|
||||
data["removed"] = True
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
console.print(f"[green]✓[/green] Automation profile removed: {name}")
|
||||
|
||||
except NotFoundError as exc:
|
||||
console.print(f"[red]Profile not found:[/red] '{name}'")
|
||||
raise typer.Abort() from exc
|
||||
except ValidationError as exc:
|
||||
console.print(f"[red]Validation Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
except CleverAgentsError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
|
||||
|
||||
@app.command("list")
|
||||
def list_profiles(
|
||||
regex: Annotated[
|
||||
str | None,
|
||||
typer.Argument(
|
||||
help="Optional regex pattern to filter profile names",
|
||||
),
|
||||
] = None,
|
||||
namespace: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--namespace",
|
||||
"-n",
|
||||
help="Filter by namespace (e.g. 'acme')",
|
||||
),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""List all automation profiles with optional filtering.
|
||||
|
||||
Shows both built-in and custom profiles. Use ``--namespace`` to
|
||||
filter by the namespace portion of namespaced names. A positional
|
||||
regex argument can be used to filter profile names.
|
||||
|
||||
Examples:
|
||||
agents automation-profile list
|
||||
agents automation-profile list --namespace acme
|
||||
agents automation-profile list "caut.*"
|
||||
agents automation-profile list --format json
|
||||
"""
|
||||
try:
|
||||
service = _get_service()
|
||||
|
||||
profiles = service.list_profiles()
|
||||
|
||||
# Deterministic ordering: sort by name
|
||||
profiles = sorted(profiles, key=lambda p: p.name)
|
||||
|
||||
# Apply namespace filter
|
||||
if namespace:
|
||||
profiles = [
|
||||
p
|
||||
for p in profiles
|
||||
if "/" in p.name and p.name.split("/", 1)[0] == namespace
|
||||
]
|
||||
|
||||
# Apply regex filter
|
||||
if regex:
|
||||
try:
|
||||
pattern = re.compile(regex)
|
||||
except re.error as exc:
|
||||
console.print(f"[red]Invalid regex pattern:[/red] {regex}")
|
||||
raise typer.Abort() from exc
|
||||
profiles = [p for p in profiles if pattern.search(p.name)]
|
||||
|
||||
if not profiles:
|
||||
console.print("[yellow]No profiles found.[/yellow]")
|
||||
return
|
||||
|
||||
# Non-rich formats
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = [_profile_spec_dict(p) for p in profiles]
|
||||
console.print(format_output(data, fmt))
|
||||
return
|
||||
|
||||
# Rich table display
|
||||
table = Table(title=f"Automation Profiles ({len(profiles)} total)")
|
||||
table.add_column("Name", style="cyan")
|
||||
table.add_column("Source", style="blue")
|
||||
table.add_column("Description", style="dim")
|
||||
table.add_column("Strategize", justify="right")
|
||||
table.add_column("Execute", justify="right")
|
||||
table.add_column("Apply", justify="right")
|
||||
table.add_column("Sandbox", justify="center")
|
||||
|
||||
for profile in profiles:
|
||||
source = "built-in" if profile.name in BUILTIN_PROFILES else "custom"
|
||||
table.add_row(
|
||||
profile.name,
|
||||
source,
|
||||
profile.description[:40]
|
||||
+ ("..." if len(profile.description) > 40 else ""),
|
||||
f"{profile.auto_strategize:.1f}",
|
||||
f"{profile.auto_execute:.1f}",
|
||||
f"{profile.auto_apply:.1f}",
|
||||
"yes" if profile.require_sandbox else "no",
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
except CleverAgentsError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
|
||||
|
||||
@app.command("show")
|
||||
def show_profile(
|
||||
name: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Name of the automation profile to show"),
|
||||
],
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Show details for an automation profile.
|
||||
|
||||
Displays all thresholds, safety requirements, and metadata for the
|
||||
specified profile.
|
||||
|
||||
Examples:
|
||||
agents automation-profile show manual
|
||||
agents automation-profile show acme/strict --format yaml
|
||||
"""
|
||||
try:
|
||||
service = _get_service()
|
||||
|
||||
profile = service.get_profile(name)
|
||||
|
||||
_print_profile(profile, title="Automation Profile Details", fmt=fmt)
|
||||
|
||||
except NotFoundError as exc:
|
||||
console.print(f"[red]Profile not found:[/red] '{name}'")
|
||||
raise typer.Abort() from exc
|
||||
except CleverAgentsError as exc:
|
||||
console.print(f"[red]Error:[/red] {exc.message}")
|
||||
raise typer.Abort() from exc
|
||||
|
||||
|
||||
def emit_automation_level_deprecation_warning() -> None:
|
||||
"""Emit a deprecation warning for the --automation-level flag.
|
||||
|
||||
Called from ``plan use`` and ``plan set-automation-level`` when the
|
||||
legacy ``--automation-level`` option is used.
|
||||
"""
|
||||
warnings.warn(
|
||||
"'--automation-level' is deprecated and will be removed in a "
|
||||
"future release. Use '--automation-profile' instead. "
|
||||
"Legacy levels are mapped to built-in profiles: "
|
||||
"manual->manual, supervised->supervised, auto->auto, "
|
||||
"full_auto->full-auto.",
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
@@ -1314,9 +1314,20 @@ def use_action(
|
||||
# Get action by name
|
||||
action = service.get_action_by_name(action_name)
|
||||
|
||||
# Parse automation level if provided
|
||||
# Parse automation level if provided (deprecated)
|
||||
resolved_automation = None
|
||||
if automation_level:
|
||||
console.print(
|
||||
"[yellow]Warning:[/yellow] '--automation-level' is deprecated. "
|
||||
"Use '--automation-profile' instead. "
|
||||
"Legacy levels are mapped to built-in profiles."
|
||||
)
|
||||
warnings.warn(
|
||||
"'--automation-level' is deprecated and will be removed "
|
||||
"in a future release. Use '--automation-profile' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
try:
|
||||
resolved_automation = AutomationLevel(automation_level.lower())
|
||||
except ValueError:
|
||||
@@ -1792,6 +1803,9 @@ def set_automation_level(
|
||||
) -> None:
|
||||
"""Change the automation level for an existing plan.
|
||||
|
||||
.. deprecated::
|
||||
Use ``--automation-profile`` on ``plan use`` instead.
|
||||
|
||||
Only affects future phase transitions. Subplans created after
|
||||
this change will inherit the new level.
|
||||
|
||||
@@ -1802,6 +1816,18 @@ def set_automation_level(
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import AutomationLevel
|
||||
|
||||
console.print(
|
||||
"[yellow]Warning:[/yellow] 'set-automation-level' is deprecated. "
|
||||
"Use '--automation-profile' on 'plan use' instead. "
|
||||
"Legacy levels are mapped to built-in profiles."
|
||||
)
|
||||
warnings.warn(
|
||||
"'plan set-automation-level' is deprecated and will be removed "
|
||||
"in a future release. Use '--automation-profile' on 'plan use' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
try:
|
||||
service = _get_lifecycle_service()
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ def _register_subcommands() -> None:
|
||||
from cleveragents.cli.commands import (
|
||||
action,
|
||||
actor,
|
||||
automation_profile,
|
||||
context,
|
||||
plan,
|
||||
project,
|
||||
@@ -122,6 +123,11 @@ def _register_subcommands() -> None:
|
||||
name="resource",
|
||||
help="Manage resources and resource types in the resource registry",
|
||||
)
|
||||
app.add_typer(
|
||||
automation_profile.app,
|
||||
name="automation-profile",
|
||||
help="Manage automation profiles that control plan execution autonomy",
|
||||
)
|
||||
|
||||
_subcommands_registered = True
|
||||
|
||||
@@ -483,6 +489,7 @@ def main(args: list[str] | None = None) -> int:
|
||||
"action", # v3 plan lifecycle actions
|
||||
"resource", # Resource registry management
|
||||
"auto-debug", # Auto-debug commands
|
||||
"automation-profile", # Automation profile management
|
||||
"tell", # Shortcut for plan tell
|
||||
"build", # Shortcut for plan build
|
||||
"apply", # Shortcut for plan apply
|
||||
|
||||
Reference in New Issue
Block a user