fix(v3.7.0): resolve issue #1468 - plan use structured panels #1514

Closed
freemo wants to merge 3 commits from fix/1468-impl into master
5 changed files with 977 additions and 6 deletions
@@ -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}"
)
@@ -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)"
@@ -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()
@@ -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
+158 -6
View File
1
@@ -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 <id>' when ready.[/dim]"
)
except ActionNotAvailableError as e:
console.print(f"[red]Action not available:[/red] {e}")