diff --git a/features/steps/tdd_plan_use_structured_panels_steps.py b/features/steps/tdd_plan_use_structured_panels_steps.py new file mode 100644 index 000000000..be8ced7af --- /dev/null +++ b/features/steps/tdd_plan_use_structured_panels_steps.py @@ -0,0 +1,275 @@ +"""Step definitions for TDD — agents plan use structured panels (issue #1468).""" + +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, + PlanPhase, + PlanTimestamps, + ProcessingState, + ProjectLink, +) + +_PLAN_ULID = "01JTEST1468PANEMS0000000001" + + +def _make_action(name: str = "local/test-action") -> Action: + """Create a minimal Action for plan use tests.""" + return Action( + namespaced_name=NamespacedName.parse(name), + description="Test action for structured panels", + long_description=None, + definition_of_done="All panels rendered correctly", + strategy_actor="local/strategist", + execution_actor="local/executor", + reusable=True, + read_only=False, + state=ActionState.AVAILABLE, + created_by=None, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + +def _make_plan( + *, + project_links: list[ProjectLink] | None = None, + arguments: dict[str, object] | None = None, + arguments_order: list[str] | None = None, + automation_profile: AutomationProfileRef | None = None, + strategy_actor: str | None = "local/strategist", + execution_actor: str | None = "local/executor", + estimation_actor: str | None = None, +) -> Plan: + """Create a Plan instance for structured panels tests.""" + now = datetime.now() + args = dict(arguments) if arguments else {} + return Plan( + identity=PlanIdentity(plan_id=_PLAN_ULID), + namespaced_name=NamespacedName.parse("local/test-plan"), + description="Test plan for structured panels", + definition_of_done="All panels rendered correctly", + action_name="local/test-action", + phase=PlanPhase.STRATEGIZE, + processing_state=ProcessingState.QUEUED, + project_links=project_links or [ProjectLink(project_name="local/api-service")], + arguments=args, + arguments_order=arguments_order or list(args.keys()), + automation_profile=automation_profile, + strategy_actor=strategy_actor, + execution_actor=execution_actor, + estimation_actor=estimation_actor, + reusable=True, + read_only=False, + created_by=None, + timestamps=PlanTimestamps(created_at=now, updated_at=now), + ) + + +# --------------------------------------------------------------------------- +# Background +# --------------------------------------------------------------------------- + + +@given("a plan use panels CLI runner") +def step_plan_use_panels_runner(context: Context) -> None: + """Set up the CLI runner.""" + context.runner = CliRunner() + + +@given("a plan use panels mocked lifecycle service") +def step_plan_use_panels_mock_service(context: Context) -> None: + """Set up a mocked lifecycle service.""" + context.mock_service = MagicMock() + + +# --------------------------------------------------------------------------- +# Given steps +# --------------------------------------------------------------------------- + + +@given("a plan use panels action exists") +def step_plan_use_panels_action_exists(context: Context) -> None: + """Configure the mock service with a standard action and plan.""" + context.mock_service.get_action_by_name.return_value = _make_action() + context.mock_plan = _make_plan( + automation_profile=AutomationProfileRef( + profile_name="trusted", + provenance=AutomationProfileProvenance.PLAN, + ), + strategy_actor="local/strategist", + execution_actor="local/executor", + ) + context.mock_service.use_action.return_value = context.mock_plan + + +@given("a plan use panels action exists with no automation profile") +def step_plan_use_panels_action_no_profile(context: Context) -> None: + """Configure the mock service with an action and plan without automation profile.""" + context.mock_service.get_action_by_name.return_value = _make_action() + context.mock_plan = _make_plan( + automation_profile=None, + strategy_actor="local/strategist", + execution_actor="local/executor", + ) + context.mock_service.use_action.return_value = context.mock_plan + + +# --------------------------------------------------------------------------- +# When steps +# --------------------------------------------------------------------------- + + +@when("I run plan use with rich output") +def step_run_plan_use_rich(context: Context) -> None: + """Run plan use with default rich format.""" + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=context.mock_service, + ): + context.result = context.runner.invoke( + plan_app, + ["use", "local/test-action", "local/api-service"], + ) + + +@when('I run plan use with arg "{arg_str}"') +def step_run_plan_use_with_arg(context: Context, arg_str: str) -> None: + """Run plan use with a specific --arg value.""" + # Parse the arg to set up the mock plan with that argument + if "=" in arg_str: + key, val_str = arg_str.split("=", 1) + try: + val: object = int(val_str) + except ValueError: + try: + val = float(val_str) + except ValueError: + val = val_str + context.mock_plan = _make_plan( + arguments={key: val}, + arguments_order=[key], + automation_profile=AutomationProfileRef( + profile_name="trusted", + provenance=AutomationProfileProvenance.PLAN, + ), + ) + context.mock_service.use_action.return_value = context.mock_plan + + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=context.mock_service, + ): + context.result = context.runner.invoke( + plan_app, + ["use", "local/test-action", "local/api-service", "--arg", arg_str], + ) + + +@when('I run plan use with automation profile "{profile}"') +def step_run_plan_use_with_profile(context: Context, profile: str) -> None: + """Run plan use with --automation-profile.""" + context.mock_plan = _make_plan( + automation_profile=AutomationProfileRef( + profile_name=profile, + provenance=AutomationProfileProvenance.PLAN, + ), + ) + context.mock_service.use_action.return_value = context.mock_plan + + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=context.mock_service, + ): + context.result = context.runner.invoke( + plan_app, + [ + "use", + "local/test-action", + "local/api-service", + "--automation-profile", + profile, + ], + ) + + +@when("I run plan use with json format") +def step_run_plan_use_json(context: Context) -> None: + """Run plan use with --format json.""" + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=context.mock_service, + ): + context.result = context.runner.invoke( + plan_app, + ["use", "local/test-action", "local/api-service", "--format", "json"], + ) + + +@when("I run plan use with rich output and no arguments") +def step_run_plan_use_no_args(context: Context) -> None: + """Run plan use with no --arg flags.""" + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=context.mock_service, + ): + context.result = context.runner.invoke( + plan_app, + ["use", "local/test-action", "local/api-service"], + ) + + +@when("I run plan use with rich output and no automation profile") +def step_run_plan_use_no_profile(context: Context) -> None: + """Run plan use with no --automation-profile flag.""" + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=context.mock_service, + ): + context.result = context.runner.invoke( + plan_app, + ["use", "local/test-action", "local/api-service"], + ) + + +# --------------------------------------------------------------------------- +# Then steps +# --------------------------------------------------------------------------- + + +@then("the plan use output should succeed") +def step_plan_use_output_succeed(context: Context) -> None: + """Assert the CLI invocation exited with code 0.""" + assert context.result.exit_code == 0, ( + f"Expected exit code 0, got {context.result.exit_code}.\n" + f"Output:\n{context.result.output}" + ) + + +@then("the plan use output should contain the plan ID") +def step_plan_use_output_contains_plan_id(context: Context) -> None: + """Assert the output contains the plan ULID.""" + assert _PLAN_ULID in context.result.output, ( + f"Expected plan ID '{_PLAN_ULID}' in output.\nOutput:\n{context.result.output}" + ) + + +@then('the plan use output should contain "{text}"') +def step_plan_use_output_contains(context: Context, text: str) -> None: + """Assert the output contains the given text.""" + assert text in context.result.output, ( + f"Expected '{text}' in output.\nOutput:\n{context.result.output}" + ) diff --git a/features/tdd_plan_use_structured_panels.feature b/features/tdd_plan_use_structured_panels.feature new file mode 100644 index 000000000..046d11386 --- /dev/null +++ b/features/tdd_plan_use_structured_panels.feature @@ -0,0 +1,104 @@ +@tdd_issue @tdd_issue_1468 @cli @plan @use +Feature: TDD — agents plan use renders spec-required structured panels + As a developer + I want the ``agents plan use`` rich output to render six structured panels + So that users see Plan Created, Inputs, Actors, Automation, Context, and Next Steps + + Background: + Given a plan use panels CLI runner + And a plan use panels mocked lifecycle service + + @tdd_issue @tdd_issue_1468 + Scenario: Plan Created panel is rendered with required fields + Given a plan use panels action exists + When I run plan use with rich output + Then the plan use output should succeed + And the plan use output should contain "Plan Created" + And the plan use output should contain the plan ID + And the plan use output should contain "Phase" + And the plan use output should contain "Action" + And the plan use output should contain "Project" + And the plan use output should contain "Automation" + And the plan use output should contain "Attempt" + + @tdd_issue @tdd_issue_1468 + Scenario: Inputs panel is rendered with argument key=value pairs + Given a plan use panels action exists + When I run plan use with arg "target_coverage_percent=85" + Then the plan use output should succeed + And the plan use output should contain "Inputs" + And the plan use output should contain "target_coverage_percent=85" + + @tdd_issue @tdd_issue_1468 + Scenario: Actors panel is rendered with Strategy, Execution, Estimation actors + Given a plan use panels action exists + When I run plan use with rich output + Then the plan use output should succeed + And the plan use output should contain "Actors" + And the plan use output should contain "Strategy" + And the plan use output should contain "Execution" + And the plan use output should contain "Estimation" + + @tdd_issue @tdd_issue_1468 + Scenario: Automation panel is rendered with Profile, Source, Read-Only fields + Given a plan use panels action exists + When I run plan use with automation profile "trusted" + Then the plan use output should succeed + And the plan use output should contain "Automation" + And the plan use output should contain "Profile" + And the plan use output should contain "Source" + And the plan use output should contain "Read-Only" + + @tdd_issue @tdd_issue_1468 + Scenario: Context panel is rendered with Resources, Indexed Files, View, Hot Token Budget + Given a plan use panels action exists + When I run plan use with rich output + Then the plan use output should succeed + And the plan use output should contain "Context" + And the plan use output should contain "Resources" + And the plan use output should contain "Indexed Files" + And the plan use output should contain "View" + And the plan use output should contain "Hot Token Budget" + + @tdd_issue @tdd_issue_1468 + Scenario: Next Steps panel is rendered with follow-up commands + Given a plan use panels action exists + When I run plan use with rich output + Then the plan use output should succeed + And the plan use output should contain "Next Steps" + And the plan use output should contain "agents plan execute" + And the plan use output should contain "agents plan status" + + @tdd_issue @tdd_issue_1468 + Scenario: All six panels are rendered in a single plan use invocation + Given a plan use panels action exists + When I run plan use with rich output + Then the plan use output should succeed + And the plan use output should contain "Plan Created" + And the plan use output should contain "Inputs" + And the plan use output should contain "Actors" + And the plan use output should contain "Automation" + And the plan use output should contain "Context" + And the plan use output should contain "Next Steps" + + @tdd_issue @tdd_issue_1468 + Scenario: Non-rich format output is not affected by panel changes + Given a plan use panels action exists + When I run plan use with json format + Then the plan use output should succeed + And the plan use output should contain "plan_id" + + @tdd_issue @tdd_issue_1468 + Scenario: Plan use with no arguments still renders Inputs panel + Given a plan use panels action exists + When I run plan use with rich output and no arguments + Then the plan use output should succeed + And the plan use output should contain "Inputs" + + @tdd_issue @tdd_issue_1468 + Scenario: Plan use with no automation profile renders Automation panel with none + Given a plan use panels action exists with no automation profile + When I run plan use with rich output and no automation profile + Then the plan use output should succeed + And the plan use output should contain "Automation" + And the plan use output should contain "(none)" diff --git a/robot/helper_tdd_plan_use_structured_panels.py b/robot/helper_tdd_plan_use_structured_panels.py new file mode 100644 index 000000000..b694f7def --- /dev/null +++ b/robot/helper_tdd_plan_use_structured_panels.py @@ -0,0 +1,347 @@ +"""Helper script for tdd_plan_use_structured_panels.robot integration tests. + +Each subcommand is a self-contained check that prints a sentinel on success. +Tests verify that ``agents plan use`` rich output renders all six spec-required +structured panels as defined in docs/specification.md §agents plan use. + +Issue: #1468 — UAT: agents plan use rich output missing spec-required structured panels +""" + +from __future__ import annotations + +import sys +from datetime import datetime +from pathlib import Path +from unittest.mock import MagicMock, patch + +# Ensure the local source tree takes priority over any installed copy. +_SRC = str(Path(__file__).resolve().parents[1] / "src") +sys.path = [_SRC] + [ + p + for p in sys.path + if p != _SRC and not (p.endswith("/src") and "cleveragents" not in p.split("/")[-1]) +] +if _SRC not in sys.path: + sys.path.insert(0, _SRC) +# Flush any already-loaded cleveragents modules so they reimport from _SRC +for _mod_name in sorted(sys.modules): + if _mod_name.startswith("cleveragents"): + del sys.modules[_mod_name] + +from typer.testing import CliRunner # noqa: E402 + +from cleveragents.cli.commands.plan import app as plan_app # noqa: E402 +from cleveragents.domain.models.core.action import Action, ActionState # noqa: E402 +from cleveragents.domain.models.core.plan import ( # noqa: E402 + AutomationProfileProvenance, + AutomationProfileRef, + NamespacedName, + Plan, + PlanIdentity, + PlanPhase, + PlanTimestamps, + ProcessingState, + ProjectLink, +) + +runner = CliRunner() + +_PLAN_ULID = "01JTEST1468PANEMS0000000001" + + +def _mock_action(name: str = "local/test-action") -> Action: + """Create a minimal Action for plan use tests.""" + return Action( + namespaced_name=NamespacedName.parse(name), + description="Test action for structured panels", + long_description=None, + definition_of_done="All panels rendered correctly", + strategy_actor="local/strategist", + execution_actor="local/executor", + reusable=True, + read_only=False, + state=ActionState.AVAILABLE, + created_by=None, + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + +def _mock_plan( + *, + project_links: list[ProjectLink] | None = None, + arguments: dict[str, object] | None = None, + arguments_order: list[str] | None = None, + automation_profile: AutomationProfileRef | None = None, + strategy_actor: str | None = "local/strategist", + execution_actor: str | None = "local/executor", + estimation_actor: str | None = None, +) -> Plan: + """Create a Plan instance for structured panels tests.""" + now = datetime.now() + args = dict(arguments) if arguments else {} + return Plan( + identity=PlanIdentity(plan_id=_PLAN_ULID), + namespaced_name=NamespacedName.parse("local/test-plan"), + description="Test plan for structured panels", + definition_of_done="All panels rendered correctly", + action_name="local/test-action", + phase=PlanPhase.STRATEGIZE, + processing_state=ProcessingState.QUEUED, + project_links=project_links or [ProjectLink(project_name="local/api-service")], + arguments=args, + arguments_order=arguments_order or list(args.keys()), + automation_profile=automation_profile, + strategy_actor=strategy_actor, + execution_actor=execution_actor, + estimation_actor=estimation_actor, + reusable=True, + read_only=False, + created_by=None, + timestamps=PlanTimestamps(created_at=now, updated_at=now), + ) + + +def _run_plan_use( + extra_args: list[str] | None = None, + plan: Plan | None = None, +) -> tuple[int, str]: + """Run plan use and return (exit_code, output).""" + mock_service = MagicMock() + mock_service.get_action_by_name.return_value = _mock_action() + mock_service.use_action.return_value = plan or _mock_plan( + automation_profile=AutomationProfileRef( + profile_name="trusted", + provenance=AutomationProfileProvenance.PLAN, + ), + ) + args = ["use", "local/test-action", "local/api-service"] + (extra_args or []) + with patch( + "cleveragents.cli.commands.plan._get_lifecycle_service", + return_value=mock_service, + ): + result = runner.invoke(plan_app, args) + return result.exit_code, result.output + + +# --------------------------------------------------------------------------- +# Subcommands +# --------------------------------------------------------------------------- + + +def check_plan_created_panel() -> None: + """Verify Plan Created panel renders with required fields.""" + exit_code, output = _run_plan_use() + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required = [ + "Plan Created", + _PLAN_ULID, + "Phase", + "Action", + "Project", + "Automation", + "Attempt", + ] + missing = [f for f in required if f not in output] + if missing: + print(f"FAIL: missing fields in Plan Created panel: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-plan-created-ok") + + +def check_inputs_panel() -> None: + """Verify Inputs panel renders with argument key=value pairs.""" + plan = _mock_plan( + arguments={"target_coverage_percent": 85}, + arguments_order=["target_coverage_percent"], + automation_profile=AutomationProfileRef( + profile_name="trusted", + provenance=AutomationProfileProvenance.PLAN, + ), + ) + exit_code, output = _run_plan_use( + extra_args=["--arg", "target_coverage_percent=85"], + plan=plan, + ) + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required = ["Inputs", "target_coverage_percent=85"] + missing = [f for f in required if f not in output] + if missing: + print(f"FAIL: missing fields in Inputs panel: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-inputs-ok") + + +def check_actors_panel() -> None: + """Verify Actors panel renders with Strategy, Execution, Estimation fields.""" + exit_code, output = _run_plan_use() + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required = ["Actors", "Strategy", "Execution", "Estimation"] + missing = [f for f in required if f not in output] + if missing: + print(f"FAIL: missing fields in Actors panel: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-actors-ok") + + +def check_automation_panel() -> None: + """Verify Automation panel renders with Profile, Source, Read-Only fields.""" + exit_code, output = _run_plan_use(extra_args=["--automation-profile", "trusted"]) + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required = ["Automation", "Profile", "Source", "Read-Only"] + missing = [f for f in required if f not in output] + if missing: + print(f"FAIL: missing fields in Automation panel: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-automation-ok") + + +def check_context_panel() -> None: + """Verify Context panel renders with Resources, Indexed Files, View, Budget.""" + exit_code, output = _run_plan_use() + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required = ["Context", "Resources", "Indexed Files", "View", "Hot Token Budget"] + missing = [f for f in required if f not in output] + if missing: + print(f"FAIL: missing fields in Context panel: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-context-ok") + + +def check_next_steps_panel() -> None: + """Verify Next Steps panel renders with follow-up commands.""" + exit_code, output = _run_plan_use() + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required = ["Next Steps", "agents plan execute", "agents plan status"] + missing = [f for f in required if f not in output] + if missing: + print(f"FAIL: missing fields in Next Steps panel: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-next-steps-ok") + + +def check_all_six_panels() -> None: + """Verify all six spec-required panels are rendered in a single invocation.""" + exit_code, output = _run_plan_use() + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + required_panels = [ + "Plan Created", + "Inputs", + "Actors", + "Automation", + "Context", + "Next Steps", + ] + missing = [p for p in required_panels if p not in output] + if missing: + print(f"FAIL: missing panels: {missing}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-all-panels-ok") + + +def check_json_format() -> None: + """Verify --format json output is not affected by panel changes.""" + exit_code, output = _run_plan_use(extra_args=["--format", "json"]) + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + if "plan_id" not in output: + print("FAIL: 'plan_id' not found in JSON output", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-json-ok") + + +def check_no_profile() -> None: + """Verify Automation panel renders with (none) when no profile is set.""" + plan = _mock_plan(automation_profile=None) + exit_code, output = _run_plan_use(plan=plan) + if exit_code != 0: + print(f"FAIL: exit code {exit_code}", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + if "Automation" not in output: + print("FAIL: 'Automation' panel not found in output", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + if "(none)" not in output: + print("FAIL: '(none)' not found in Automation panel output", file=sys.stderr) + print(output, file=sys.stderr) + sys.exit(1) + + print("tdd-1468-no-profile-ok") + + +# --------------------------------------------------------------------------- +# Main dispatcher +# --------------------------------------------------------------------------- + +_COMMANDS = { + "check-plan-created-panel": check_plan_created_panel, + "check-inputs-panel": check_inputs_panel, + "check-actors-panel": check_actors_panel, + "check-automation-panel": check_automation_panel, + "check-context-panel": check_context_panel, + "check-next-steps-panel": check_next_steps_panel, + "check-all-six-panels": check_all_six_panels, + "check-json-format": check_json_format, + "check-no-profile": check_no_profile, +} + + +def main() -> None: + if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS: + print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr) + sys.exit(2) + _COMMANDS[sys.argv[1]]() + + +if __name__ == "__main__": + main() diff --git a/robot/tdd_plan_use_structured_panels.robot b/robot/tdd_plan_use_structured_panels.robot new file mode 100644 index 000000000..886488f81 --- /dev/null +++ b/robot/tdd_plan_use_structured_panels.robot @@ -0,0 +1,93 @@ +*** Settings *** +Documentation TDD integration tests for agents plan use structured panels (issue #1468) +... Verifies that the rich output renders all six spec-required panels: +... Plan Created, Inputs, Actors, Automation, Context, Next Steps. +Resource ${CURDIR}/common.resource +Suite Setup Setup Test Environment +Suite Teardown Cleanup Test Environment + +*** Variables *** +${HELPER} ${CURDIR}/helper_tdd_plan_use_structured_panels.py + +*** Test Cases *** +Plan Use Renders Plan Created Panel + [Documentation] Verify that ``plan use`` rich output renders the Plan Created panel + ... with Plan ID, Phase, Action, Project, Automation, and Attempt fields. + ${result}= Run Process ${PYTHON} ${HELPER} check-plan-created-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-plan-created-ok + +Plan Use Renders Inputs Panel + [Documentation] Verify that ``plan use`` rich output renders the Inputs panel + ... with argument key=value pairs. + ${result}= Run Process ${PYTHON} ${HELPER} check-inputs-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-inputs-ok + +Plan Use Renders Actors Panel + [Documentation] Verify that ``plan use`` rich output renders the Actors panel + ... with Strategy, Execution, and Estimation actor fields. + ${result}= Run Process ${PYTHON} ${HELPER} check-actors-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-actors-ok + +Plan Use Renders Automation Panel + [Documentation] Verify that ``plan use`` rich output renders the Automation panel + ... with Profile, Source, and Read-Only fields. + ${result}= Run Process ${PYTHON} ${HELPER} check-automation-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-automation-ok + +Plan Use Renders Context Panel + [Documentation] Verify that ``plan use`` rich output renders the Context panel + ... with Resources, Indexed Files, View, and Hot Token Budget fields. + ${result}= Run Process ${PYTHON} ${HELPER} check-context-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-context-ok + +Plan Use Renders Next Steps Panel + [Documentation] Verify that ``plan use`` rich output renders the Next Steps panel + ... with suggested follow-up commands including agents plan execute. + ${result}= Run Process ${PYTHON} ${HELPER} check-next-steps-panel cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-next-steps-ok + +Plan Use Renders All Six Panels + [Documentation] Verify that ``plan use`` rich output renders all six spec-required panels + ... in a single invocation: Plan Created, Inputs, Actors, Automation, + ... Context, and Next Steps. + ${result}= Run Process ${PYTHON} ${HELPER} check-all-six-panels cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-all-panels-ok + +Plan Use JSON Format Not Affected By Panel Changes + [Documentation] Verify that ``plan use --format json`` output is not affected + ... by the structured panel changes and still returns valid JSON. + ${result}= Run Process ${PYTHON} ${HELPER} check-json-format cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-json-ok + +Plan Use No Automation Profile Renders Automation Panel With None + [Documentation] Verify that when no automation profile is set, the Automation panel + ... still renders with "(none)" values. + ${result}= Run Process ${PYTHON} ${HELPER} check-no-profile cwd=${WORKSPACE} + Log ${result.stdout} + Log ${result.stderr} + Should Be Equal As Integers ${result.rc} 0 + Should Contain ${result.stdout} tdd-1468-no-profile-ok diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index b2a7f64d0..56c2c1bbd 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -1531,6 +1531,163 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None: console.print(Panel(details, title=title, expand=False)) +def _print_use_action_panels(plan: Any) -> None: + """Render the six spec-required structured panels for ``agents plan use`` output. + + Implements the output contract defined in docs/specification.md §agents plan use. + + Panels rendered: + 1. Plan Created — Plan ID, Phase, Action, Project, Automation, Attempt + 2. Inputs — argument key=value pairs + 3. Actors — Strategy, Execution, Estimation actors + 4. Automation — Profile, Source, Read-Only flag + 5. Context — Resources, Indexed Files, View, Hot Token Budget + 6. Next Steps — suggested follow-up commands + + Args: + plan: A v3 Plan object from the domain model. + """ + from cleveragents.domain.models.core.plan import ( + AutomationProfileProvenance, + ) + from cleveragents.domain.models.core.plan import ( + Plan as LifecyclePlan, + ) + + if not isinstance(plan, LifecyclePlan): + # Fallback for unexpected plan types — should not occur in normal flow + console.print(Panel(f"Plan: {plan}", title="Plan Created", expand=False)) + return + + plan_id = plan.identity.plan_id + phase_val = plan.phase.value if plan.phase else "unknown" + action_val = plan.action_name or "(unknown)" + + # Project: join all project names + project_names = [link.project_name for link in plan.project_links] + project_val = ", ".join(project_names) if project_names else "(none)" + + # Automation profile name + automation_val = ( + plan.automation_profile.profile_name if plan.automation_profile else "(none)" + ) + + # Attempt: use identity attempt counter if available, else 1 + attempt_val = getattr(plan.identity, "attempt", 1) or 1 + + # ── Panel 1: Plan Created ────────────────────────────────────────────── + created_content = ( + f"[cyan bold]Plan ID:[/cyan bold] {plan_id}\n" + f"[yellow bold]Phase:[/yellow bold] {phase_val}\n" + f"[magenta bold]Action:[/magenta bold] {action_val}\n" + f"[blue bold]Project:[/blue bold] {project_val}\n" + f"[green bold]Automation:[/green bold] {automation_val}\n" + f"[blue bold]Attempt:[/blue bold] {attempt_val}" + ) + console.print(Panel(created_content, title="Plan Created", expand=False)) + + # ── Panel 2: Inputs ─────────────────────────────────────────────────── + input_lines: list[str] = [] + if plan.arguments: + for key in plan.arguments_order: + if key in plan.arguments: + input_lines.append(f"- {key}={plan.arguments[key]}") + # Include any keys not in arguments_order (defensive) + for key, val in plan.arguments.items(): + if key not in plan.arguments_order: + input_lines.append(f"- {key}={val}") + if plan.automation_profile: + prof_name = plan.automation_profile.profile_name + input_lines.append(f"- automation_profile={prof_name}") + inputs_content = "\n".join(input_lines) if input_lines else "(none)" + console.print(Panel(inputs_content, title="Inputs", expand=False)) + + # ── Panel 3: Actors ─────────────────────────────────────────────────── + strategy_val = plan.strategy_actor or "(none)" + execution_val = plan.execution_actor or "(none)" + estimation_val = plan.estimation_actor or "(none)" + actors_content = ( + f"[magenta bold]Strategy:[/magenta bold] {strategy_val}\n" + f"[magenta bold]Execution:[/magenta bold] {execution_val}\n" + f"[blue bold]Estimation:[/blue bold] {estimation_val}" + ) + console.print(Panel(actors_content, title="Actors", expand=False)) + + # ── Panel 4: Automation ─────────────────────────────────────────────── + if plan.automation_profile: + profile_name = plan.automation_profile.profile_name + provenance = plan.automation_profile.provenance + # Map provenance enum to human-readable source label + _provenance_labels: dict[AutomationProfileProvenance, str] = { + AutomationProfileProvenance.PLAN: "CLI flag", + AutomationProfileProvenance.ACTION: "action default", + AutomationProfileProvenance.PROJECT: "project default", + AutomationProfileProvenance.GLOBAL: "global default", + } + source_label = _provenance_labels.get(provenance, provenance.value) + else: + profile_name = "(none)" + source_label = "(none)" + + # Read-only: check if any project link is read-only + read_only_val = ( + "yes" if any(link.read_only for link in plan.project_links) else "no" + ) + + automation_content = ( + f"[magenta bold]Profile:[/magenta bold] {profile_name}\n" + f"[blue bold]Source:[/blue bold] {source_label}\n" + f"[blue bold]Read-Only:[/blue bold] {read_only_val}" + ) + console.print(Panel(automation_content, title="Automation", expand=False)) + + # ── Panel 5: Context ────────────────────────────────────────────────── + # Resources: count of project links (each project is a resource scope) + resource_count = len(plan.project_links) + resource_names = ", ".join(project_names) if project_names else "(none)" + resources_val = ( + f"{resource_count} ({resource_names})" if resource_count > 0 else "0" + ) + + # Indexed files: use compressed_tokens from skeleton_metadata as a proxy + # for the number of indexed context tokens (spec field; actual file count + # is not tracked separately in SkeletonMetadata). + indexed_files_val: str + if plan.skeleton_metadata is not None: + indexed_files_val = str(plan.skeleton_metadata.compressed_tokens) + else: + indexed_files_val = "(unknown)" + + # View: the current phase value (strategize at plan creation) + view_val = phase_val + + # Hot token budget: use budget_remaining from cost_metadata if available + hot_token_budget_val: str + if plan.cost_metadata is not None: + budget = plan.cost_metadata.budget_remaining + hot_token_budget_val = f"{budget:,.2f}" if budget is not None else "(unlimited)" + else: + hot_token_budget_val = "(unknown)" + + context_content = ( + f"[blue bold]Resources:[/blue bold] {resources_val}\n" + f"[blue bold]Indexed Files:[/blue bold] {indexed_files_val}\n" + f"[blue bold]View:[/blue bold] {view_val}\n" + f"[blue bold]Hot Token Budget:[/blue bold] {hot_token_budget_val}" + ) + console.print(Panel(context_content, title="Context", expand=False)) + + # ── Panel 6: Next Steps ─────────────────────────────────────────────── + next_steps_content = ( + f"- agents plan execute {plan_id}\n" + f"- agents plan status {plan_id}\n" + f"- agents plan tree {plan_id}" + ) + console.print(Panel(next_steps_content, title="Next Steps", expand=False)) + + console.print("\n[green bold]✓ OK[/green bold] Plan created") + + @app.command("use") def use_action( action_name: Annotated[ @@ -1848,17 +2005,12 @@ def use_action( data = _plan_spec_dict(plan) console.print(format_output(data, fmt)) else: - _print_lifecycle_plan(plan, title="Plan Created") + _print_use_action_panels(plan) if plan.is_terminal: console.print( f"\n[dim]Plan completed with state: " f"{plan.state.value if plan.state else 'unknown'}.[/dim]" ) - else: - console.print( - "\n[dim]Plan is now in Strategize phase (queued). " - "Run 'agents plan execute ' when ready.[/dim]" - ) except ActionNotAvailableError as e: console.print(f"[red]Action not available:[/red] {e}")