fix(cli): add agents plan start alias or update spec to reflect v3 plan use/execute commands
CI / lint (pull_request) Failing after 34s
CI / quality (pull_request) Successful in 48s
CI / security (pull_request) Successful in 57s
CI / typecheck (pull_request) Successful in 1m4s
CI / coverage (pull_request) Has been skipped
CI / build (pull_request) Successful in 24s
CI / push-validation (pull_request) Successful in 17s
CI / helm (pull_request) Successful in 47s
CI / e2e_tests (pull_request) Successful in 4m48s
CI / unit_tests (pull_request) Failing after 16m2s
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 22m56s
CI / status-check (pull_request) Has been cancelled

- Added 'agents plan start' as an alias for 'agents plan use' to match v3 spec
- Added 'agents plan show' as an alias for 'agents plan status' to match v3 spec
- Both commands delegate to their canonical counterparts with full feature parity
- Updated module docstring to document the new aliases
- Added BDD tests for both new commands with comprehensive scenarios
- Updated CHANGELOG.md with the new feature entry
This commit is contained in:
2026-04-13 21:54:30 +00:00
parent 5438540803
commit d3a6f57daa
4 changed files with 556 additions and 0 deletions
+6
View File
@@ -7,6 +7,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- **Plan CLI Spec Alignment** (#8628): Added `agents plan start` as an alias for
`agents plan use` and `agents plan show` as an alias for `agents plan status`
to match the v3 specification. Both commands delegate to their canonical
counterparts while maintaining full feature parity. Updated module docstring
to document the new aliases.
- **TDD Issue-Capture Test Activation** (#7025): Replaced 234 bare `@skip` tags
across 82 Behave feature files with the correct `@tdd_expected_fail @tdd_issue
@tdd_issue_<N>` tag system. Scenarios whose referenced bugs were already fixed
@@ -0,0 +1,70 @@
Feature: Plan CLI start and show command aliases
As a developer following the v3 spec
I want to use `agents plan start` and `agents plan show` commands
So that the CLI matches the specification documentation
Background:
Given a plan start show CLI runner
And a plan start show mocked lifecycle service
# ---- agents plan start: alias for agents plan use ----
Scenario: Plan start creates a plan (alias for plan use)
Given a plan start show action exists
When I run plan start with action "local/test-action" and project "proj-1"
Then the plan start should succeed
And the plan start should create a plan in Strategize phase
Scenario: Plan start with multiple projects
Given a plan start show action exists
When I run plan start with action "local/test-action" and projects "proj-1" and "proj-2"
Then the plan start should succeed
And the plan start should link projects "proj-1" and "proj-2"
Scenario: Plan start with --arg option
Given a plan start show action exists
When I run plan start with action "local/test-action" project "proj-1" and arg "target_coverage=80"
Then the plan start should succeed
And the plan start should pass argument "target_coverage" with value 80
Scenario: Plan start with --automation-profile
Given a plan start show action exists
When I run plan start with action "local/test-action" project "proj-1" and automation profile "trusted"
Then the plan start should succeed
And the plan start output should contain "Automation Profile"
Scenario: Plan start with --invariant
Given a plan start show action exists
When I run plan start with action "local/test-action" project "proj-1" and invariant "No warnings"
Then the plan start should succeed
And the plan start should pass invariant "No warnings"
# ---- agents plan show: alias for agents plan status ----
Scenario: Plan show displays plan status (alias for plan status)
Given a plan start show plan exists for show
When I run plan show for the plan
Then the plan show should succeed
And the plan show output should contain "Phase"
And the plan show output should contain "Processing State"
Scenario: Plan show with no arguments lists all plans
Given plan start show plans exist
When I run plan show with no arguments
Then the plan show should succeed
And the plan show output should contain "Active Plans"
Scenario: Plan show displays plan details
Given a plan start show plan exists for show
When I run plan show for the plan
Then the plan show should succeed
And the plan show output should contain "Action"
And the plan show output should contain "Projects"
And the plan show output should contain "Arguments"
# ---- Help text verification ----
Scenario: Plan start command appears in help
When I run plan help
Then the help output should contain "start"
Scenario: Plan show command appears in help
When I run plan help
Then the help output should contain "show"
@@ -0,0 +1,327 @@
"""Step definitions for plan CLI start and show command aliases."""
from __future__ import annotations
from datetime import datetime
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.plan import app as plan_app
from cleveragents.domain.models.core.action import Action, ActionState
from cleveragents.domain.models.core.plan import (
AutomationProfileProvenance,
AutomationProfileRef,
NamespacedName,
Plan,
PlanIdentity,
PlanInvariant,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
_PLAN_ULID = "01KHDE6WWS2171PWW3GJEBXZ8S"
def _make_plan(
*,
name: str = "local/test-plan",
action_name: str = "local/test-action",
phase: PlanPhase = PlanPhase.STRATEGIZE,
state: ProcessingState = ProcessingState.QUEUED,
project_links: list[ProjectLink] | None = None,
arguments: dict[str, object] | None = None,
arguments_order: list[str] | None = None,
automation_profile: AutomationProfileRef | None = None,
invariants: list[PlanInvariant] | None = None,
strategy_actor: str | None = "openai/gpt-4",
execution_actor: str | None = "openai/gpt-4",
estimation_actor: str | None = None,
invariant_actor: str | None = None,
) -> Plan:
"""Create a Plan instance for start/show tests."""
now = datetime.now()
return Plan(
identity=PlanIdentity(plan_id=_PLAN_ULID),
namespaced_name=NamespacedName.parse(name),
description="Test plan description",
definition_of_done="All tests pass",
action_name=action_name,
phase=phase,
processing_state=state,
project_links=project_links or [],
arguments=dict(arguments) if arguments else {},
arguments_order=arguments_order or [],
automation_profile=automation_profile,
invariants=invariants or [],
strategy_actor=strategy_actor,
execution_actor=execution_actor,
estimation_actor=estimation_actor,
invariant_actor=invariant_actor,
reusable=True,
read_only=False,
created_by=None,
timestamps=PlanTimestamps(created_at=now, updated_at=now),
)
def _make_action(name: str = "local/test-action") -> Action:
"""Create an Action for plan start tests."""
return Action(
namespaced_name=NamespacedName.parse(name),
description="Test action",
long_description=None,
definition_of_done="All tests pass",
strategy_actor="openai/gpt-4",
execution_actor="openai/gpt-4",
reusable=True,
read_only=False,
state=ActionState.AVAILABLE,
created_by=None,
created_at=datetime.now(),
updated_at=datetime.now(),
)
# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------
@given("a plan start show CLI runner")
def step_plan_start_show_runner(context: Context) -> None:
"""Set up the CLI runner."""
context.runner = CliRunner()
@given("a plan start show mocked lifecycle service")
def step_plan_start_show_mocked_service(context: Context) -> None:
"""Mock the lifecycle service."""
context.mock_service = MagicMock()
context.mock_service.use_action.return_value = _make_plan()
context.mock_service.get_plan.return_value = _make_plan()
context.mock_service.list_plans.return_value = [_make_plan()]
context.mock_service.get_action_by_name.return_value = _make_action()
# ---------------------------------------------------------------------------
# Plan start (alias for plan use)
# ---------------------------------------------------------------------------
@given("a plan start show action exists")
def step_plan_start_show_action_exists(context: Context) -> None:
"""Ensure an action exists for testing."""
context.action = _make_action()
@when('I run plan start with action "{action}" and project "{project}"')
def step_run_plan_start_single_project(
context: Context, action: str, project: str
) -> None:
"""Run plan start with a single project."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(plan_app, ["start", action, project])
@when('I run plan start with action "{action}" and projects "{proj1}" and "{proj2}"')
def step_run_plan_start_multiple_projects(
context: Context, action: str, proj1: str, proj2: str
) -> None:
"""Run plan start with multiple projects."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(
plan_app, ["start", action, proj1, proj2]
)
@when('I run plan start with action "{action}" project "{project}" and arg "{arg}"')
def step_run_plan_start_with_arg(
context: Context, action: str, project: str, arg: str
) -> None:
"""Run plan start with an argument."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(
plan_app, ["start", action, project, "--arg", arg]
)
@when(
'I run plan start with action "{action}" project "{project}" and automation profile "{profile}"'
)
def step_run_plan_start_with_profile(
context: Context, action: str, project: str, profile: str
) -> None:
"""Run plan start with automation profile."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(
plan_app,
["start", action, project, "--automation-profile", profile],
)
@when(
'I run plan start with action "{action}" project "{project}" and invariant "{invariant}"'
)
def step_run_plan_start_with_invariant(
context: Context, action: str, project: str, invariant: str
) -> None:
"""Run plan start with an invariant."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(
plan_app, ["start", action, project, "--invariant", invariant]
)
@then("the plan start should succeed")
def step_plan_start_should_succeed(context: Context) -> None:
"""Verify plan start succeeded."""
assert context.result.exit_code == 0, (
f"Expected exit code 0, got {context.result.exit_code}. "
f"Output: {context.result.stdout}"
)
@then("the plan start should create a plan in Strategize phase")
def step_plan_start_creates_plan(context: Context) -> None:
"""Verify plan was created in Strategize phase."""
context.mock_service.use_action.assert_called_once()
@then('the plan start should link projects "{proj1}" and "{proj2}"')
def step_plan_start_links_projects(context: Context, proj1: str, proj2: str) -> None:
"""Verify projects were linked."""
context.mock_service.use_action.assert_called_once()
call_args = context.mock_service.use_action.call_args
project_links = call_args.kwargs.get("project_links", [])
project_names = [p.project_name for p in project_links]
assert proj1 in project_names, f"Project {proj1} not found in {project_names}"
assert proj2 in project_names, f"Project {proj2} not found in {project_names}"
@then('the plan start should pass argument "{arg_name}" with value {arg_value}')
def step_plan_start_passes_argument(
context: Context, arg_name: str, arg_value: str
) -> None:
"""Verify argument was passed."""
context.mock_service.use_action.assert_called_once()
call_args = context.mock_service.use_action.call_args
arguments = call_args.kwargs.get("arguments", {})
assert arg_name in arguments, f"Argument {arg_name} not found in {arguments}"
# Convert arg_value to int if it looks like a number
try:
expected_value = int(arg_value)
except ValueError:
expected_value = arg_value
assert arguments[arg_name] == expected_value
@then('the plan start should pass invariant "{invariant}"')
def step_plan_start_passes_invariant(context: Context, invariant: str) -> None:
"""Verify invariant was passed."""
context.mock_service.use_action.assert_called_once()
call_args = context.mock_service.use_action.call_args
invariants = call_args.kwargs.get("invariants", [])
invariant_texts = [inv.text for inv in invariants]
assert invariant in invariant_texts, (
f"Invariant {invariant} not found in {invariant_texts}"
)
@then('the plan start output should contain "{text}"')
def step_plan_start_output_contains(context: Context, text: str) -> None:
"""Verify output contains text."""
assert text in context.result.stdout, (
f"Expected '{text}' in output, got: {context.result.stdout}"
)
# ---------------------------------------------------------------------------
# Plan show (alias for plan status)
# ---------------------------------------------------------------------------
@given("a plan start show plan exists for show")
def step_plan_start_show_plan_exists(context: Context) -> None:
"""Ensure a plan exists for show testing."""
context.plan = _make_plan()
@given("plan start show plans exist")
def step_plan_start_show_plans_exist(context: Context) -> None:
"""Ensure multiple plans exist."""
context.plans = [
_make_plan(name="local/plan-1"),
_make_plan(name="local/plan-2"),
]
context.mock_service.list_plans.return_value = context.plans
@when("I run plan show for the plan")
def step_run_plan_show_for_plan(context: Context) -> None:
"""Run plan show for a specific plan."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(plan_app, ["show", _PLAN_ULID])
@when("I run plan show with no arguments")
def step_run_plan_show_no_args(context: Context) -> None:
"""Run plan show with no arguments (list all plans)."""
with patch(
"cleveragents.cli.commands.plan._get_lifecycle_service",
return_value=context.mock_service,
):
context.result = context.runner.invoke(plan_app, ["show"])
@when("I run plan help")
def step_run_plan_help(context: Context) -> None:
"""Run plan help to see available commands."""
context.result = context.runner.invoke(plan_app, ["--help"])
@then("the plan show should succeed")
def step_plan_show_should_succeed(context: Context) -> None:
"""Verify plan show succeeded."""
assert context.result.exit_code == 0, (
f"Expected exit code 0, got {context.result.exit_code}. "
f"Output: {context.result.stdout}"
)
@then('the plan show output should contain "{text}"')
def step_plan_show_output_contains(context: Context, text: str) -> None:
"""Verify output contains text."""
assert text in context.result.stdout, (
f"Expected '{text}' in output, got: {context.result.stdout}"
)
@then('the help output should contain "{text}"')
def step_help_output_contains(context: Context, text: str) -> None:
"""Verify help output contains text."""
assert text in context.result.stdout, (
f"Expected '{text}' in help output, got: {context.result.stdout}"
)
+153
View File
@@ -8,8 +8,10 @@ plan lifecycle.
| Command | Description |
|-------------------------------|-----------------------------------------|
| ``agents plan use`` | Create plan from action + project(s) |
| ``agents plan start`` | Create plan (alias for ``use``) |
| ``agents plan list`` | List plans with optional filters |
| ``agents plan status`` | Show plan status / details |
| ``agents plan show`` | Show plan status (alias for ``status``) |
| ``agents plan execute`` | Run phase-aware plan execution |
| ``agents plan apply`` | Transition to Apply phase |
| ``agents plan cancel`` | Cancel a non-terminal plan |
@@ -2263,6 +2265,131 @@ def use_action(
raise typer.Abort() from e
@app.command("start")
def start_action(
action_name: Annotated[
str,
typer.Argument(help="Action name to use"),
],
projects: Annotated[
list[str] | None,
typer.Argument(help="Projects to apply the action on (one or more)"),
] = None,
project: Annotated[
list[str] | None,
typer.Option(
"--project",
"-p",
help=(
"Project name to use the action on "
"(can be repeated for multiple projects)"
),
),
] = None,
arg: Annotated[
list[str] | None,
typer.Option(
"--arg",
"-a",
help="Argument value (format: name=value)",
),
] = None,
automation_profile: Annotated[
str | None,
typer.Option(
"--automation-profile",
help="Automation profile name to use for this plan",
),
] = None,
invariant: Annotated[
list[str] | None,
typer.Option(
"--invariant",
help="Invariant constraint text (repeatable)",
),
] = None,
strategy_actor: Annotated[
str | None,
typer.Option(
"--strategy-actor",
help="Override the strategy actor for this plan",
),
] = None,
execution_actor: Annotated[
str | None,
typer.Option(
"--execution-actor",
help="Override the execution actor for this plan",
),
] = None,
estimation_actor: Annotated[
str | None,
typer.Option(
"--estimation-actor",
help="Override the estimation actor for this plan",
),
] = None,
invariant_actor: Annotated[
str | None,
typer.Option(
"--invariant-actor",
help="Override the invariant reconciliation actor for this plan",
),
] = None,
execution_environment: Annotated[
str | None,
typer.Option(
"--execution-environment",
help="Execution environment: host or container",
),
] = None,
execution_env_priority: Annotated[
str | None,
typer.Option(
"--execution-env-priority",
help="Priority semantics: fallback (default) or override",
),
] = None,
fmt: Annotated[
str,
typer.Option(
"--format",
"-f",
help=_FORMAT_HELP,
),
] = "rich",
) -> None:
"""Start a plan using an action on projects (alias for 'agents plan use').
This command is an alias for 'agents plan use' to match the v3 specification.
It creates a plan in Strategize phase from an action.
The first positional argument is the ACTION name. Subsequent positional
arguments are PROJECT names. Projects can also be supplied via the
repeatable ``--project`` / ``-p`` option.
Examples:
agents plan start local/code-coverage proj-1 proj-2 --arg target_coverage=80
agents plan start local/lint --project proj-1 --invariant "No new warnings"
"""
# Delegate to use_action with the same parameters
return use_action(
action_name=action_name,
projects=projects,
project=project,
arg=arg,
automation_profile=automation_profile,
invariant=invariant,
strategy_actor=strategy_actor,
execution_actor=execution_actor,
estimation_actor=estimation_actor,
invariant_actor=invariant_actor,
execution_environment=execution_environment,
execution_env_priority=execution_env_priority,
fmt=fmt,
)
@app.command("execute")
def execute_plan(
plan_id: Annotated[
@@ -2742,6 +2869,32 @@ def plan_status(
raise typer.Abort() from e
@app.command("show")
def show_plan(
plan_id: Annotated[
str | None,
typer.Argument(help="Plan ID to show status for"),
] = None,
fmt: Annotated[
str,
typer.Option(
"--format",
"-f",
help=_FORMAT_HELP,
),
] = "rich",
) -> None:
"""Show status of a v3 lifecycle plan (alias for 'agents plan status').
This command is an alias for 'agents plan status' to match the v3 specification.
Displays the current phase, state, and other details.
When no plan ID is given, lists all active plans.
"""
# Delegate to plan_status with the same parameters
return plan_status(plan_id=plan_id, fmt=fmt)
@app.command("errors")
def plan_errors(
plan_id: Annotated[