From 603dc841b01ce70c09728b85d13ed83d78fbf97e Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Sun, 29 Mar 2026 07:35:06 +0000 Subject: [PATCH] refactor(cli): move context commands to actor context subgroup Register context as a sub-app of the actor Typer group so commands are accessible under the canonical specification path `agents actor context`. The top-level `agents context` group remains as a deprecated alias (with Typer deprecated=True) for one release cycle. Key changes: - context.app registered under actor.app in main.py - `rm` subcommand renamed to `remove`; `rm` kept as hidden deprecated alias that prints a deprecation warning before delegating - Programmatic rm_command() deprecated in favor of remove_command() - Help text updated to reference canonical `agents actor context` paths - Behave and Robot tests updated to use canonical paths and renamed subcommand; assertion for context help lowercased for flexibility - All nox sessions pass: lint, typecheck, unit_tests (495 features, 12731 scenarios), coverage at 97% ISSUES CLOSED: #888 --- features/cli_plan_context_commands.feature | 4 +- features/steps/cli_commands_coverage_steps.py | 4 +- features/steps/context_unit_tests_steps.py | 28 +++++------ robot/cli_plan_context_commands.robot | 8 ++-- src/cleveragents/cli/commands/context.py | 47 +++++++++++++++++-- src/cleveragents/cli/main.py | 18 ++++++- 6 files changed, 80 insertions(+), 29 deletions(-) diff --git a/features/cli_plan_context_commands.feature b/features/cli_plan_context_commands.feature index 6fc1e52e2..6a5d3aa32 100644 --- a/features/cli_plan_context_commands.feature +++ b/features/cli_plan_context_commands.feature @@ -116,7 +116,7 @@ Feature: CLI Plan and Context Commands - Complete Coverage Scenario: Remove file from context Given I have an initialized project And I have added files "main.py", "utils.py" to context - When I run "cleveragents context rm main.py" + When I run "cleveragents actor context remove main.py" Then the command should succeed And "main.py" should not be in context And "utils.py" should still be in context @@ -157,7 +157,7 @@ Feature: CLI Plan and Context Commands - Complete Coverage Scenario: Cannot remove non-existent context file Given I have an initialized project - When I run "cleveragents context rm non-existent.py" + When I run "cleveragents actor context remove non-existent.py" Then the command should fail And the error message should mention "not in context" diff --git a/features/steps/cli_commands_coverage_steps.py b/features/steps/cli_commands_coverage_steps.py index 908aa738e..858989fb6 100644 --- a/features/steps/cli_commands_coverage_steps.py +++ b/features/steps/cli_commands_coverage_steps.py @@ -135,7 +135,7 @@ def step_run_context_remove(context): else: file_to_remove = "dummy_file.py" - result = runner.invoke(context_app, ["rm", file_to_remove]) + result = runner.invoke(context_app, ["remove", file_to_remove]) context.result = result @@ -424,7 +424,7 @@ def step_verify_context_help(context): """Verify context help displayed.""" assert hasattr(context, "result") assert context.result.exit_code == 0 - assert "Context management" in context.result.output + assert "context management" in context.result.output.lower() @then("the plan help should display") diff --git a/features/steps/context_unit_tests_steps.py b/features/steps/context_unit_tests_steps.py index d5034504b..c0ff4bf46 100644 --- a/features/steps/context_unit_tests_steps.py +++ b/features/steps/context_unit_tests_steps.py @@ -307,7 +307,7 @@ def step_exec_context_remove(context): project_service.get_current_project.return_value = object() container.project_service.return_value = project_service - result = runner.invoke(context_app.app, ["rm", "/file1.txt", "/file2.txt"]) + result = runner.invoke(context_app.app, ["remove", "/file1.txt", "/file2.txt"]) context.result = result @@ -323,7 +323,7 @@ def step_exec_context_remove_no_project(context): project_service.get_current_project.return_value = None container.project_service.return_value = project_service - result = runner.invoke(context_app.app, ["rm", "/file1.txt"]) + result = runner.invoke(context_app.app, ["remove", "/file1.txt"]) context.result = result @@ -339,7 +339,7 @@ def step_exec_context_remove_relative(context): project_service.get_current_project.return_value = object() container.project_service.return_value = project_service - result = runner.invoke(context_app.app, ["rm", "file1.txt", "file2.txt"]) + result = runner.invoke(context_app.app, ["remove", "file1.txt", "file2.txt"]) context.result = result @@ -356,7 +356,7 @@ def step_exec_context_remove_failure(context): project_service.get_current_project.return_value = object() container.project_service.return_value = project_service - result = runner.invoke(context_app.app, ["rm", "/file1.txt"]) + result = runner.invoke(context_app.app, ["remove", "/file1.txt"]) context.result = result @@ -822,8 +822,8 @@ def step_check_empty_message(context): f"Clean output (no color codes): '{clean_output}'" ) - assert "agents context add" in clean_output, ( - f"Expected help text 'agents context add' in output.\n" + assert "agents actor context add" in clean_output, ( + f"Expected help text 'agents actor context add' in output.\n" f"Raw output: '{context.result.output}'\n" f"Clean output (no color codes): '{clean_output}'" ) @@ -1166,47 +1166,47 @@ def step_then_show_returns_content(context): @when("the programmatic remove command runs without an active project") def step_when_programmatic_remove_no_project(context): - """Execute rm_command without an active project configured.""" + """Execute remove_command without an active project configured.""" context.programmatic_exception = None with patched_container(context, project=None): try: - context_app.rm_command(["missing.txt"]) + context_app.remove_command(["missing.txt"]) except Exception as exc: context.programmatic_exception = exc @when("the programmatic remove command runs for files") def step_when_programmatic_remove(context): - """Execute rm_command where files are missing.""" + """Execute remove_command where files are missing.""" context.programmatic_exception = None with patched_container(context, project=object()): try: - context_app.rm_command(["missing.txt", "other.txt"]) + context_app.remove_command(["missing.txt", "other.txt"]) except Exception as exc: context.programmatic_exception = exc @when("the programmatic remove command runs with removable files") def step_when_programmatic_remove_success(context): - """Execute rm_command when files are present in context.""" + """Execute remove_command when files are present in context.""" context.programmatic_exception = None context.mock_service.remove_from_context.reset_mock() with patched_container(context, project=object()): try: - context_app.rm_command(["/file1.txt", "/file2.txt"]) + context_app.remove_command(["/file1.txt", "/file2.txt"]) except Exception as exc: context.programmatic_exception = exc @then("a CleverAgentsError is raised for programmatic remove") def step_then_remove_requires_presence(context): - """Ensure rm_command raises when files are not in context.""" + """Ensure remove_command raises when files are not in context.""" assert isinstance(context.programmatic_exception, CleverAgentsError) @then("the programmatic remove command should complete successfully") def step_then_remove_success(context): - """Ensure rm_command succeeds when files are removed.""" + """Ensure remove_command succeeds when files are removed.""" assert context.programmatic_exception is None assert context.mock_service.remove_from_context.call_count == 2 diff --git a/robot/cli_plan_context_commands.robot b/robot/cli_plan_context_commands.robot index 7664dbe21..f974f2c14 100644 --- a/robot/cli_plan_context_commands.robot +++ b/robot/cli_plan_context_commands.robot @@ -198,17 +198,17 @@ Show Context Content [Teardown] Cleanup Test Directory Remove File From Context - [Documentation] Test context rm command + [Documentation] Test actor context remove command [Setup] Initialize Test Project With Context - # Remove test.py - ${result}= Run Process ${PYTHON} -m cleveragents context rm test.py + # Remove test.py via canonical path: agents actor context remove + ${result}= Run Process ${PYTHON} -m cleveragents actor context remove test.py ... cwd=${TEST_DIR} timeout=120s on_timeout=kill Should Be Equal As Integers ${result.rc} 0 # Verify removal - ${result}= Run Process ${PYTHON} -m cleveragents context list + ${result}= Run Process ${PYTHON} -m cleveragents actor context list ... cwd=${TEST_DIR} timeout=120s on_timeout=kill Should Not Contain ${result.stdout} test.py diff --git a/src/cleveragents/cli/commands/context.py b/src/cleveragents/cli/commands/context.py index 1e107af37..1e4a902bd 100644 --- a/src/cleveragents/cli/commands/context.py +++ b/src/cleveragents/cli/commands/context.py @@ -2,6 +2,9 @@ This module implements context-related commands for managing files and directories that the AI will work with. + +Canonical path: ``agents actor context `` +Deprecated alias: ``agents context `` (emits deprecation warning) """ from __future__ import annotations @@ -23,7 +26,9 @@ if TYPE_CHECKING: from cleveragents.domain.models.core import Context # Create sub-app for context commands -app = typer.Typer(help="Context management commands") +app = typer.Typer( + help="Actor context management commands (canonical: agents actor context)" +) console = _get_console() @@ -142,7 +147,7 @@ def show_command(path: str | None = None) -> str | None: return f"Total files: {len(context_files)}" -def rm_command(paths: list[str]) -> None: +def remove_command(paths: list[str]) -> None: """Programmatic interface for removing files from context. Args: @@ -177,6 +182,18 @@ def rm_command(paths: list[str]) -> None: raise CleverAgentsError(f"File(s) not in context: {', '.join(not_in_context)}") +def rm_command(paths: list[str]) -> None: + """Deprecated alias for :func:`remove_command`.""" + import warnings + + warnings.warn( + "rm_command is deprecated; use remove_command instead.", + DeprecationWarning, + stacklevel=2, + ) + remove_command(paths) + + def clear_command() -> None: """Programmatic interface for clearing all context files.""" from cleveragents.application.container import get_container @@ -285,7 +302,7 @@ def context_load( context_add(paths, recursive) -@app.command("rm") +@app.command("remove") def context_remove( paths: Annotated[ list[str], @@ -358,6 +375,25 @@ def context_remove( raise typer.Abort() from e +@app.command("rm", hidden=True) +def context_rm_deprecated( + paths: Annotated[ + list[str], + typer.Argument(help="Paths to remove from context"), + ], +) -> None: + """Remove files from context (deprecated: use 'remove' instead).""" + import warnings + + warnings.warn( + "'agents context rm' is deprecated; use 'agents actor context remove' instead.", + DeprecationWarning, + stacklevel=1, + ) + console.print("[yellow]Warning:[/yellow] 'rm' is deprecated; use 'remove' instead.") + context_remove(paths) + + @app.command("list") def context_list( context_dir: Annotated[ @@ -412,7 +448,7 @@ def context_list( if not context_files: console.print("[yellow]No files in context.[/yellow]") - console.print("Use 'agents context add ' to add files.") + console.print("Use 'agents actor context add ' to add files.") return # Display context files @@ -519,7 +555,8 @@ def context_show( console.print(f"Total size: {total_size:,} bytes") console.print( - "\nUse 'agents context show ' to view specific file content." + "\nUse 'agents actor context show ' to view" + " specific file content." ) except CleverAgentsError as e: diff --git a/src/cleveragents/cli/main.py b/src/cleveragents/cli/main.py index 7afcb56c1..a81720cbe 100644 --- a/src/cleveragents/cli/main.py +++ b/src/cleveragents/cli/main.py @@ -110,7 +110,21 @@ def _register_subcommands() -> None: return app.add_typer(project.app, name="project", help="Project management") - app.add_typer(context.app, name="context", help="Context management") + + # Register context as a sub-app of actor (canonical: agents actor context) + actor.app.add_typer( + context.app, + name="context", + help="Actor context management commands", + ) + + # Keep top-level 'agents context' as deprecated alias + app.add_typer( + context.app, + name="context", + help="Context management (deprecated: use 'agents actor context' instead)", + deprecated=True, + ) app.add_typer( plan.app, name="plan", @@ -236,7 +250,7 @@ def _print_basic_help() -> None: typer.echo("Usage: cleveragents [OPTIONS] COMMAND [ARGS]...") typer.echo("\nCommon commands:") typer.echo(" project Project management") - typer.echo(" context Context management") + typer.echo(" actor context Actor context management") typer.echo(" plan Plan operations (actor required)") typer.echo(" actor Actor management and defaults") typer.echo(" init Initialize a project") -- 2.52.0