fix(cli): add Progress, Timing, Execution Detail, and Cost panels to agents plan status output #9461
@@ -162,3 +162,87 @@ Feature: Plan lifecycle CLI coverage
|
||||
Scenario: Plan execute shares lifecycle service instance with executor
|
||||
When I run plan execute verifying lifecycle service sharing
|
||||
Then the plan executor should receive the same lifecycle service instance
|
||||
|
||||
# ---- _print_lifecycle_plan five-panel output ----
|
||||
|
||||
Scenario: Plan status renders Plan Status panel with required fields
|
||||
When I run plan status for a plan with all five panels
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Plan Status"
|
||||
And the plan lifecycle output should contain "Processing State"
|
||||
And the plan lifecycle output should contain "Projects"
|
||||
And the plan lifecycle output should contain "Action"
|
||||
And the plan lifecycle output should contain "Phase"
|
||||
And the plan lifecycle output should contain "Attempt"
|
||||
|
||||
Scenario: Plan status renders Progress panel
|
||||
When I run plan status for a plan with all five panels
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Progress"
|
||||
And the plan lifecycle output should contain "Strategize"
|
||||
And the plan lifecycle output should contain "Execute"
|
||||
And the plan lifecycle output should contain "Apply"
|
||||
|
||||
Scenario: Plan status renders Timing panel
|
||||
When I run plan status for a plan with all five panels
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Timing"
|
||||
And the plan lifecycle output should contain "Started"
|
||||
And the plan lifecycle output should contain "Elapsed"
|
||||
And the plan lifecycle output should contain "ETA"
|
||||
|
||||
Scenario: Plan status renders Execution Detail panel
|
||||
When I run plan status for a plan with all five panels
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Execution Detail"
|
||||
And the plan lifecycle output should contain "Sandbox"
|
||||
And the plan lifecycle output should contain "Tool Calls"
|
||||
And the plan lifecycle output should contain "Files Modified"
|
||||
And the plan lifecycle output should contain "Child Plans"
|
||||
And the plan lifecycle output should contain "Checkpoints"
|
||||
|
||||
Scenario: Plan status renders Cost panel
|
||||
When I run plan status for a plan with all five panels
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Cost"
|
||||
And the plan lifecycle output should contain "Tokens Used"
|
||||
And the plan lifecycle output should contain "Cost So Far"
|
||||
And the plan lifecycle output should contain "Estimated"
|
||||
|
||||
Scenario: Plan status renders status refresh footer
|
||||
When I run plan status for a plan with all five panels
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Status refreshed"
|
||||
|
||||
Scenario: Plan status renders Arguments when plan has arguments
|
||||
When I run plan status for a plan with arguments
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Arguments"
|
||||
And the plan lifecycle output should contain "coverage"
|
||||
|
||||
Scenario: Plan status renders Automation Profile when set
|
||||
When I run plan status for a plan with automation profile
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Automation Profile"
|
||||
And the plan lifecycle output should contain "trusted"
|
||||
|
||||
Scenario: Plan status renders Estimation Actor when set
|
||||
When I run plan status for a plan with estimation actor
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Estimation Actor"
|
||||
And the plan lifecycle output should contain "openai/gpt-4"
|
||||
|
||||
Scenario: Plan status renders Invariant Actor when set
|
||||
When I run plan status for a plan with invariant actor
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Invariant Actor"
|
||||
And the plan lifecycle output should contain "openai/gpt-4"
|
||||
|
||||
Scenario: Plan status renders phase timestamps when populated
|
||||
When I run plan status for a plan with all timestamps
|
||||
Then the plan lifecycle command should succeed
|
||||
And the plan lifecycle output should contain "Strategize Started"
|
||||
And the plan lifecycle output should contain "Strategize Completed"
|
||||
And the plan lifecycle output should contain "Execute Started"
|
||||
And the plan lifecycle output should contain "Execute Completed"
|
||||
And the plan lifecycle output should contain "Applied At"
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
"""Step definitions for plan_lifecycle_cli_coverage.feature.
|
||||
|
||||
Covers the five-panel output of _print_lifecycle_plan():
|
||||
- Plan Status panel (Processing State, Projects, Action, Phase, Attempt)
|
||||
- Progress panel (Strategize, Execute, Apply)
|
||||
- Timing panel (Started, Elapsed, ETA, phase timestamps)
|
||||
- Execution Detail panel (Sandbox, Tool Calls, Files Modified, Child Plans,
|
||||
Checkpoints)
|
||||
- Cost panel (Tokens Used, Cost So Far, Estimated)
|
||||
- Status refresh footer
|
||||
- Optional fields: Arguments, Automation Profile, Estimation Actor,
|
||||
Invariant Actor
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from behave import when
|
||||
|
||||
from cleveragents.cli.commands.plan import app as plan_app
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
PlanTimestamps,
|
||||
ProcessingState,
|
||||
ProjectLink,
|
||||
)
|
||||
|
||||
_PLAN_ULID = "01ARZ3NDEKTSV4RRFFQ69G5FBF"
|
||||
|
||||
|
||||
def _make_five_panel_plan(
|
||||
*,
|
||||
arguments: dict[str, object] | None = None,
|
||||
arguments_order: list[str] | None = None,
|
||||
automation_profile: AutomationProfileRef | None = None,
|
||||
estimation_actor: str | None = None,
|
||||
invariant_actor: str | None = None,
|
||||
timestamps: PlanTimestamps | None = None,
|
||||
) -> Plan:
|
||||
"""Build a Plan with all fields needed for five-panel output testing."""
|
||||
now = datetime.now()
|
||||
if timestamps is None:
|
||||
timestamps = PlanTimestamps(
|
||||
created_at=now - timedelta(hours=1),
|
||||
updated_at=now,
|
||||
)
|
||||
return Plan(
|
||||
identity=PlanIdentity(plan_id=_PLAN_ULID),
|
||||
namespaced_name=NamespacedName.parse("local/five-panel-plan"),
|
||||
action_name="local/test-action",
|
||||
description="Five panel test plan",
|
||||
phase=PlanPhase.EXECUTE,
|
||||
processing_state=ProcessingState.PROCESSING,
|
||||
project_links=[
|
||||
ProjectLink(project_name="proj-a"),
|
||||
ProjectLink(project_name="proj-b"),
|
||||
],
|
||||
arguments=dict(arguments) if arguments else {},
|
||||
arguments_order=arguments_order or [],
|
||||
automation_profile=automation_profile,
|
||||
estimation_actor=estimation_actor,
|
||||
invariant_actor=invariant_actor,
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
timestamps=timestamps,
|
||||
)
|
||||
|
||||
|
||||
@when("I run plan status for a plan with all five panels")
|
||||
def step_plan_status_five_panels(context) -> None:
|
||||
"""Run plan status for a plan that exercises all five output panels."""
|
||||
plan = _make_five_panel_plan()
|
||||
context.lifecycle_service.get_plan.return_value = plan
|
||||
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
|
||||
|
||||
|
||||
@when("I run plan status for a plan with arguments")
|
||||
def step_plan_status_with_arguments(context) -> None:
|
||||
"""Run plan status for a plan that has arguments."""
|
||||
plan = _make_five_panel_plan(
|
||||
arguments={"coverage": 80, "framework": "behave"},
|
||||
arguments_order=["coverage", "framework"],
|
||||
)
|
||||
context.lifecycle_service.get_plan.return_value = plan
|
||||
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
|
||||
|
||||
|
||||
@when("I run plan status for a plan with automation profile")
|
||||
def step_plan_status_with_automation_profile(context) -> None:
|
||||
"""Run plan status for a plan that has an automation profile."""
|
||||
plan = _make_five_panel_plan(
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="trusted",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
),
|
||||
)
|
||||
context.lifecycle_service.get_plan.return_value = plan
|
||||
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
|
||||
|
||||
|
||||
@when("I run plan status for a plan with estimation actor")
|
||||
def step_plan_status_with_estimation_actor(context) -> None:
|
||||
"""Run plan status for a plan that has an estimation actor."""
|
||||
plan = _make_five_panel_plan(estimation_actor="openai/gpt-4")
|
||||
context.lifecycle_service.get_plan.return_value = plan
|
||||
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
|
||||
|
||||
|
||||
@when("I run plan status for a plan with invariant actor")
|
||||
def step_plan_status_with_invariant_actor(context) -> None:
|
||||
"""Run plan status for a plan that has an invariant actor."""
|
||||
plan = _make_five_panel_plan(invariant_actor="openai/gpt-4")
|
||||
context.lifecycle_service.get_plan.return_value = plan
|
||||
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
|
||||
|
||||
|
||||
@when("I run plan status for a plan with all timestamps")
|
||||
def step_plan_status_with_all_timestamps(context) -> None:
|
||||
"""Run plan status for a plan that has all phase timestamps populated."""
|
||||
now = datetime.now()
|
||||
timestamps = PlanTimestamps(
|
||||
created_at=now - timedelta(hours=2),
|
||||
updated_at=now,
|
||||
strategize_started_at=now - timedelta(hours=1, minutes=50),
|
||||
strategize_completed_at=now - timedelta(hours=1, minutes=40),
|
||||
execute_started_at=now - timedelta(hours=1, minutes=30),
|
||||
execute_completed_at=now - timedelta(hours=1),
|
||||
applied_at=now - timedelta(minutes=30),
|
||||
)
|
||||
plan = _make_five_panel_plan(
|
||||
timestamps=timestamps,
|
||||
)
|
||||
context.lifecycle_service.get_plan.return_value = plan
|
||||
context.result = context.runner.invoke(plan_app, ["status", _PLAN_ULID])
|
||||
|
||||
@@ -53,7 +53,12 @@ from cleveragents.domain.models.core.error_recovery import (
|
||||
ErrorCategory,
|
||||
classify_error,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import PlanPhase, ProcessingState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
ExecutionEnvPriority,
|
||||
PlanPhase,
|
||||
ProcessingState,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
|
||||
from cleveragents.infrastructure.sandbox.git_worktree import (
|
||||
GitWorktreeSandbox,
|
||||
)
|
||||
@@ -346,8 +351,6 @@ def _execute_output_dict(
|
||||
Returns:
|
||||
A JSON-serialisable dict matching the spec-required envelope.
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import Plan as LifecyclePlan
|
||||
|
||||
if not isinstance(plan, LifecyclePlan):
|
||||
# Legacy plan fallback — return minimal envelope
|
||||
return {
|
||||
@@ -1417,19 +1420,21 @@ def _get_plan_executor(
|
||||
|
||||
|
||||
def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
"""Print v3 lifecycle plan details in a nice panel.
|
||||
"""Print v3 lifecycle plan details in multiple panels.
|
||||
|
||||
Renders five panels per specification:
|
||||
1. Plan Status - basic plan info (ID, phase, processing state, action,
|
||||
projects, arguments, automation profile, actors, created/updated)
|
||||
2. Progress - Strategize/Execute/Apply step progress
|
||||
3. Timing - Started, Elapsed, ETA, and all phase timestamps
|
||||
4. Execution Detail - Sandbox, Tool Calls, Files Modified, Child Plans,
|
||||
Checkpoints
|
||||
5. Cost - Tokens Used, Cost So Far, Estimated
|
||||
|
||||
Args:
|
||||
plan: A v3 Plan object from plan.py (not plan_legacy.py)
|
||||
title: Panel title
|
||||
title: Panel title (used for Plan Status panel)
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
ExecutionEnvPriority,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
Plan as LifecyclePlan,
|
||||
)
|
||||
|
||||
if not isinstance(plan, LifecyclePlan):
|
||||
# Fall back to legacy plan display - this shouldn't happen
|
||||
console.print(Panel(f"Plan: {plan}", title=title, expand=False))
|
||||
@@ -1448,28 +1453,85 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
project_parts.append(label)
|
||||
projects = ", ".join(project_parts) if project_parts else "(none)"
|
||||
|
||||
description_preview = plan.description[:200]
|
||||
if len(plan.description) > 200:
|
||||
description_preview = f"{description_preview}..."
|
||||
|
||||
details = (
|
||||
f"[bold]ID:[/bold] {plan.identity.plan_id}\n"
|
||||
f"[bold]Name:[/bold] {plan.namespaced_name}\n"
|
||||
f"[bold]Action:[/bold] {plan.action_name}\n"
|
||||
f"[bold]Phase:[/bold] {plan.phase.value}\n"
|
||||
f"[bold]Processing State:[/bold] {state_display}\n"
|
||||
f"[bold]Projects:[/bold] {projects}\n"
|
||||
f"[bold]Description:[/bold]\n {description_preview}\n"
|
||||
f"[bold]Strategy Actor:[/bold] {plan.strategy_actor or '(not set)'}\n"
|
||||
f"[bold]Execution Actor:[/bold] {plan.execution_actor or '(not set)'}\n"
|
||||
# ===== PANEL 1: Plan Status =====
|
||||
plan_status_details = (
|
||||
f"[cyan bold]Plan:[/cyan bold] {plan.identity.plan_id}\n"
|
||||
f"[yellow bold]Phase:[/yellow bold] {plan.phase.value}\n"
|
||||
f"[magenta bold]Processing State:[/magenta bold] {state_display}\n"
|
||||
f"[#5599ff bold]Action:[/#5599ff bold] {plan.action_name}\n"
|
||||
f"[#66cc66 bold]Projects:[/#66cc66 bold] {projects}\n"
|
||||
f"[#5599ff bold]Attempt:[/#5599ff bold] {plan.identity.attempt}"
|
||||
)
|
||||
|
||||
# Definition-of-Done evaluation summary
|
||||
# Arguments (ordered)
|
||||
if plan.arguments:
|
||||
ordered_keys = (
|
||||
plan.arguments_order
|
||||
if plan.arguments_order
|
||||
else sorted(plan.arguments.keys())
|
||||
)
|
||||
args_lines = "\n".join(
|
||||
f" {key} = {plan.arguments[key]}"
|
||||
for key in ordered_keys
|
||||
if key in plan.arguments
|
||||
)
|
||||
plan_status_details += f"\n[bold]Arguments:[/bold]\n{args_lines}"
|
||||
|
||||
# Automation profile info
|
||||
if plan.automation_profile:
|
||||
plan_status_details += (
|
||||
f"\n[#5599ff bold]Automation Profile:[/#5599ff bold] "
|
||||
f"{plan.automation_profile.profile_name} "
|
||||
f"(source: {plan.automation_profile.provenance.value})"
|
||||
)
|
||||
|
||||
# Optional actors
|
||||
if plan.strategy_actor:
|
||||
plan_status_details += f"\n[bold]Strategy Actor:[/bold] {plan.strategy_actor}"
|
||||
if plan.execution_actor:
|
||||
plan_status_details += f"\n[bold]Execution Actor:[/bold] {plan.execution_actor}"
|
||||
if plan.estimation_actor:
|
||||
plan_status_details += (
|
||||
f"\n[bold]Estimation Actor:[/bold] {plan.estimation_actor}"
|
||||
)
|
||||
if plan.invariant_actor:
|
||||
plan_status_details += f"\n[bold]Invariant Actor:[/bold] {plan.invariant_actor}"
|
||||
|
||||
# Execution environment
|
||||
if plan.execution_environment:
|
||||
plan_status_details += (
|
||||
f"\n[bold]Execution Environment:[/bold] {plan.execution_environment}"
|
||||
)
|
||||
priority_display = (
|
||||
plan.execution_env_priority.value
|
||||
if plan.execution_env_priority is not None
|
||||
else ExecutionEnvPriority.FALLBACK.value
|
||||
)
|
||||
plan_status_details += (
|
||||
f"\n[bold]Execution Env Priority:[/bold] {priority_display}"
|
||||
)
|
||||
|
||||
# Timestamps: Created and Updated always shown
|
||||
plan_status_details += (
|
||||
f"\n[bold]Created:[/bold] {plan.timestamps.created_at}"
|
||||
f"\n[bold]Updated:[/bold] {plan.timestamps.updated_at}"
|
||||
)
|
||||
|
||||
# Description (truncated to 200 chars)
|
||||
if plan.description:
|
||||
description_preview = plan.description[:200]
|
||||
if len(plan.description) > 200:
|
||||
description_preview = f"{description_preview}..."
|
||||
plan_status_details += f"\n[bold]Description:[/bold]\n {description_preview}"
|
||||
|
||||
# Definition-of-Done
|
||||
if plan.definition_of_done:
|
||||
dod_preview = plan.definition_of_done[:200]
|
||||
if len(plan.definition_of_done) > 200:
|
||||
dod_preview += "..."
|
||||
details += f"[bold]Definition of Done:[/bold]\n {dod_preview}\n"
|
||||
plan_status_details += f"\n[bold]Definition of Done:[/bold]\n {dod_preview}"
|
||||
|
||||
# DoD evaluation summary
|
||||
if plan.validation_summary and plan.validation_summary.get("dod_evaluated"):
|
||||
vs = plan.validation_summary
|
||||
dod_passed = vs.get("dod_all_passed", False)
|
||||
@@ -1478,102 +1540,46 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
dod_total = req_pass + req_fail
|
||||
status_color = "green" if dod_passed else "red"
|
||||
status_label = "PASSED" if dod_passed else "FAILED"
|
||||
details += (
|
||||
f"[bold]DoD Evaluation:[/bold] "
|
||||
plan_status_details += (
|
||||
f"\n[bold]DoD Evaluation:[/bold] "
|
||||
f"[{status_color}]{status_label}[/{status_color}]"
|
||||
f" ({req_pass}/{dod_total} passed"
|
||||
f"{f', {req_fail} failed' if req_fail else ''})\n"
|
||||
)
|
||||
|
||||
# Execution environment
|
||||
if plan.execution_environment:
|
||||
details += f"[bold]Execution Environment:[/bold] {plan.execution_environment}\n"
|
||||
priority_display = (
|
||||
plan.execution_env_priority.value
|
||||
if plan.execution_env_priority is not None
|
||||
else ExecutionEnvPriority.FALLBACK.value
|
||||
)
|
||||
details += f"[bold]Execution Env Priority:[/bold] {priority_display}\n"
|
||||
|
||||
# Optional actors
|
||||
if plan.estimation_actor:
|
||||
details += f"[bold]Estimation Actor:[/bold] {plan.estimation_actor}\n"
|
||||
if plan.invariant_actor:
|
||||
details += f"[bold]Invariant Actor:[/bold] {plan.invariant_actor}\n"
|
||||
|
||||
# Estimation results
|
||||
# TODO: Migrate to EstimationResult.as_display_dict() to reduce duplication
|
||||
if plan.estimation_result is not None:
|
||||
est = plan.estimation_result
|
||||
details += "[bold]Estimation:[/bold]\n"
|
||||
if est.summary:
|
||||
details += f" Summary: {est.summary}\n"
|
||||
if est.estimated_cost_usd is not None:
|
||||
details += f" Est. Cost: ${est.estimated_cost_usd:.4f}\n"
|
||||
if est.estimated_tokens is not None:
|
||||
details += f" Est. Tokens: {est.estimated_tokens}\n"
|
||||
if est.estimated_steps is not None:
|
||||
details += f" Est. Steps: {est.estimated_steps}\n"
|
||||
if est.estimated_child_plans is not None:
|
||||
details += f" Est. Child Plans: {est.estimated_child_plans}\n"
|
||||
if est.estimated_time_seconds is not None:
|
||||
details += f" Est. Time: {est.estimated_time_seconds:.1f}s\n"
|
||||
if est.risk_level is not None:
|
||||
details += f" Risk Level: {est.risk_level}\n"
|
||||
if est.risk_factors:
|
||||
details += " Risk Factors:\n"
|
||||
for factor in est.risk_factors:
|
||||
details += f" - {factor}\n"
|
||||
|
||||
# Arguments (ordered)
|
||||
if plan.arguments:
|
||||
details += "[bold]Arguments:[/bold]\n"
|
||||
ordered_keys = (
|
||||
plan.arguments_order
|
||||
if plan.arguments_order
|
||||
else sorted(plan.arguments.keys())
|
||||
)
|
||||
for key in ordered_keys:
|
||||
if key in plan.arguments:
|
||||
details += f" {key} = {plan.arguments[key]}\n"
|
||||
|
||||
# Automation profile info
|
||||
if plan.automation_profile:
|
||||
details += (
|
||||
f"[bold]Automation Profile:[/bold] "
|
||||
f"{plan.automation_profile.profile_name} "
|
||||
f"(source: {plan.automation_profile.provenance.value})\n"
|
||||
f"{f', {req_fail} failed' if req_fail else ''})"
|
||||
)
|
||||
|
||||
# Invariants
|
||||
if plan.invariants:
|
||||
details += "[bold]Invariants:[/bold]\n"
|
||||
plan_status_details += "\n[bold]Invariants:[/bold]"
|
||||
for inv in plan.invariants:
|
||||
details += f" [{inv.source.value}] {inv.text}\n"
|
||||
plan_status_details += f"\n [{inv.source.value}] {inv.text}"
|
||||
|
||||
# Resume metadata
|
||||
if plan.last_completed_step >= 0:
|
||||
details += f"[bold]Last Completed Step:[/bold] {plan.last_completed_step}\n"
|
||||
plan_status_details += (
|
||||
f"\n[bold]Last Completed Step:[/bold] {plan.last_completed_step}"
|
||||
)
|
||||
if plan.last_checkpoint_id:
|
||||
details += f"[bold]Last Checkpoint:[/bold] {plan.last_checkpoint_id}\n"
|
||||
plan_status_details += (
|
||||
f"\n[bold]Last Checkpoint:[/bold] {plan.last_checkpoint_id}"
|
||||
)
|
||||
|
||||
# Multi-project changeset summaries (#199)
|
||||
# Multi-project changeset summaries
|
||||
if (
|
||||
plan.multi_project_metadata is not None
|
||||
and plan.multi_project_metadata.project_scopes
|
||||
):
|
||||
details += "[bold]Multi-Project Scopes:[/bold]\n"
|
||||
plan_status_details += "\n[bold]Multi-Project Scopes:[/bold]"
|
||||
for scope in plan.multi_project_metadata.project_scopes:
|
||||
label = scope.project_name
|
||||
if scope.alias:
|
||||
label += f" (alias: {scope.alias})"
|
||||
if scope.read_only:
|
||||
label += " [read-only]"
|
||||
details += f" {label}\n"
|
||||
plan_status_details += f"\n {label}"
|
||||
if scope.changeset_summary is not None:
|
||||
cs = scope.changeset_summary
|
||||
details += (
|
||||
f" Changed: {cs.files_changed} "
|
||||
plan_status_details += (
|
||||
f"\n Changed: {cs.files_changed} "
|
||||
f"Added: {cs.files_added} "
|
||||
f"Deleted: {cs.files_deleted} "
|
||||
f"Lines: {cs.total_lines_changed}"
|
||||
@@ -1581,39 +1587,202 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
if cs.validation_passed is not None:
|
||||
v_color = "green" if cs.validation_passed else "red"
|
||||
v_label = "PASSED" if cs.validation_passed else "FAILED"
|
||||
details += f" Validation: [{v_color}]{v_label}[/{v_color}]"
|
||||
details += "\n"
|
||||
plan_status_details += (
|
||||
f" Validation: [{v_color}]{v_label}[/{v_color}]"
|
||||
)
|
||||
|
||||
details += f"[bold]Terminal:[/bold] {'yes' if plan.is_terminal else 'no'}\n"
|
||||
# Error message
|
||||
if plan.error_message:
|
||||
plan_status_details += f"\n[bold red]Error:[/bold red] {plan.error_message}"
|
||||
|
||||
console.print(Panel(plan_status_details, title=title, expand=False))
|
||||
|
||||
# ===== PANEL 2: Progress =====
|
||||
# Determine status of each phase based on current phase and state
|
||||
strategize_status = "✓"
|
||||
strategize_color = "green"
|
||||
execute_status = "⏳"
|
||||
execute_color = "cyan"
|
||||
apply_status = "•"
|
||||
apply_color = "yellow"
|
||||
|
||||
if plan.phase == PlanPhase.ACTION:
|
||||
strategize_status = "•"
|
||||
strategize_color = "yellow"
|
||||
execute_status = "•"
|
||||
execute_color = "yellow"
|
||||
apply_status = "•"
|
||||
apply_color = "yellow"
|
||||
elif plan.phase == PlanPhase.STRATEGIZE:
|
||||
if plan.processing_state == ProcessingState.COMPLETE:
|
||||
strategize_status = "✓"
|
||||
strategize_color = "green"
|
||||
execute_status = "•"
|
||||
execute_color = "yellow"
|
||||
else:
|
||||
strategize_status = "⏳"
|
||||
strategize_color = "cyan"
|
||||
execute_status = "•"
|
||||
execute_color = "yellow"
|
||||
elif plan.phase == PlanPhase.EXECUTE:
|
||||
strategize_status = "✓"
|
||||
strategize_color = "green"
|
||||
if plan.processing_state == ProcessingState.COMPLETE:
|
||||
execute_status = "✓"
|
||||
execute_color = "green"
|
||||
apply_status = "•"
|
||||
apply_color = "yellow"
|
||||
else:
|
||||
execute_status = "⏳"
|
||||
execute_color = "cyan"
|
||||
apply_status = "•"
|
||||
apply_color = "yellow"
|
||||
elif plan.phase == PlanPhase.APPLY:
|
||||
strategize_status = "✓"
|
||||
strategize_color = "green"
|
||||
execute_status = "✓"
|
||||
execute_color = "green"
|
||||
applied_or_constrained = plan.processing_state in (
|
||||
ProcessingState.APPLIED,
|
||||
ProcessingState.CONSTRAINED,
|
||||
)
|
||||
if applied_or_constrained:
|
||||
apply_status = "✓"
|
||||
apply_color = "green"
|
||||
else:
|
||||
apply_status = "⏳"
|
||||
apply_color = "cyan"
|
||||
|
||||
progress_details = (
|
||||
f"[{strategize_color}]{strategize_status}[/{strategize_color}] Strategize\n"
|
||||
f"[{execute_color}]{execute_status}[/{execute_color}] Execute\n"
|
||||
f"[{apply_color}]{apply_status}[/{apply_color}] Apply"
|
||||
)
|
||||
console.print(Panel(progress_details, title="Progress", expand=False))
|
||||
|
||||
# ===== PANEL 3: Timing =====
|
||||
# Calculate timing information
|
||||
started_time = "N/A"
|
||||
elapsed_time = "N/A"
|
||||
eta_time = "N/A"
|
||||
|
||||
# Timestamps
|
||||
details += f"[bold]Created:[/bold] {plan.timestamps.created_at}\n"
|
||||
details += f"[bold]Updated:[/bold] {plan.timestamps.updated_at}"
|
||||
if plan.timestamps.strategize_started_at:
|
||||
details += (
|
||||
started_time = plan.timestamps.strategize_started_at.strftime("%H:%M:%S")
|
||||
|
||||
# Calculate elapsed time
|
||||
now = datetime.now(plan.timestamps.strategize_started_at.tzinfo)
|
||||
elapsed_seconds = (now - plan.timestamps.strategize_started_at).total_seconds()
|
||||
hours = int(elapsed_seconds // 3600)
|
||||
minutes = int((elapsed_seconds % 3600) // 60)
|
||||
seconds = int(elapsed_seconds % 60)
|
||||
elapsed_time = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
|
||||
|
||||
# Calculate ETA using estimation result when available
|
||||
if (
|
||||
plan.estimation_result is not None
|
||||
and plan.estimation_result.estimated_time_seconds is not None
|
||||
):
|
||||
estimated_total_seconds = plan.estimation_result.estimated_time_seconds
|
||||
remaining_seconds = max(0, estimated_total_seconds - elapsed_seconds)
|
||||
eta_hours = int(remaining_seconds // 3600)
|
||||
eta_minutes = int((remaining_seconds % 3600) // 60)
|
||||
eta_secs = int(remaining_seconds % 60)
|
||||
eta_time = f"{eta_hours:02d}:{eta_minutes:02d}:{eta_secs:02d}"
|
||||
else:
|
||||
eta_time = "N/A"
|
||||
|
||||
timing_details = (
|
||||
f"[#66cc66 bold]Started:[/#66cc66 bold] {started_time}\n"
|
||||
f"[yellow bold]Elapsed:[/yellow bold] {elapsed_time}\n"
|
||||
f"[#66cc66 bold]ETA:[/#66cc66 bold] {eta_time}"
|
||||
)
|
||||
|
||||
# Optional phase timestamps
|
||||
if plan.timestamps.strategize_started_at:
|
||||
timing_details += (
|
||||
f"\n[bold]Strategize Started:[/bold] "
|
||||
f"{plan.timestamps.strategize_started_at}"
|
||||
)
|
||||
if plan.timestamps.strategize_completed_at:
|
||||
details += (
|
||||
timing_details += (
|
||||
f"\n[bold]Strategize Completed:[/bold] "
|
||||
f"{plan.timestamps.strategize_completed_at}"
|
||||
)
|
||||
if plan.timestamps.execute_started_at:
|
||||
details += (
|
||||
timing_details += (
|
||||
f"\n[bold]Execute Started:[/bold] {plan.timestamps.execute_started_at}"
|
||||
)
|
||||
if plan.timestamps.execute_completed_at:
|
||||
details += (
|
||||
timing_details += (
|
||||
f"\n[bold]Execute Completed:[/bold] {plan.timestamps.execute_completed_at}"
|
||||
)
|
||||
if plan.timestamps.applied_at:
|
||||
details += f"\n[bold]Applied At:[/bold] {plan.timestamps.applied_at}"
|
||||
timing_details += f"\n[bold]Applied At:[/bold] {plan.timestamps.applied_at}"
|
||||
|
||||
if plan.error_message:
|
||||
details += f"\n[bold red]Error:[/bold red] {plan.error_message}"
|
||||
console.print(Panel(timing_details, title="Timing", expand=False))
|
||||
|
||||
console.print(Panel(details, title=title, expand=False))
|
||||
# ===== PANEL 4: Execution Detail =====
|
||||
# Extract execution details
|
||||
sandbox_strategy = "N/A"
|
||||
if plan.sandbox_refs:
|
||||
# Note: sandbox_refs are internal ULID references; the spec example
|
||||
# shows a human-readable strategy name (e.g. 'git_worktree'). N/A
|
||||
# would be more accurate but raw refs are retained for diagnostic use.
|
||||
sandbox_strategy = ", ".join(plan.sandbox_refs[:3])
|
||||
if len(plan.sandbox_refs) > 3:
|
||||
sandbox_strategy += f" (+{len(plan.sandbox_refs) - 3} more)"
|
||||
|
||||
# tool_calls is not tracked in CostMetadata; display N/A
|
||||
tool_calls_display = "N/A"
|
||||
# files_modified is not directly available; display N/A
|
||||
files_modified_display = "N/A"
|
||||
|
||||
child_plans = "0/0 complete"
|
||||
if plan.subplan_statuses:
|
||||
completed = sum(
|
||||
1 for sp in plan.subplan_statuses if sp.completed_at is not None
|
||||
)
|
||||
child_plans = f"{completed}/{len(plan.subplan_statuses)} complete"
|
||||
|
||||
# Checkpoint count approximation: we only know whether at least one
|
||||
# checkpoint exists (via last_checkpoint_id). A more accurate count
|
||||
# would require an additional query; displayed as 'N/A' would be
|
||||
# consistent with tool_calls and files_modified, but the binary signal
|
||||
# (0 vs 1) is intentional for now.
|
||||
checkpoints = 0
|
||||
if plan.last_checkpoint_id:
|
||||
checkpoints = 1 # At least one checkpoint exists
|
||||
|
||||
execution_details = (
|
||||
f"[#5599ff bold]Sandbox:[/#5599ff bold] {sandbox_strategy}\n"
|
||||
f"[#5599ff bold]Tool Calls:[/#5599ff bold] {tool_calls_display}\n"
|
||||
f"[#5599ff bold]Files Modified:[/#5599ff bold] {files_modified_display}\n"
|
||||
f"[#5599ff bold]Child Plans:[/#5599ff bold] {child_plans}\n"
|
||||
f"[#5599ff bold]Checkpoints:[/#5599ff bold] {checkpoints} created"
|
||||
)
|
||||
console.print(Panel(execution_details, title="Execution Detail", expand=False))
|
||||
|
||||
# ===== PANEL 5: Cost =====
|
||||
tokens_used = 0
|
||||
cost_so_far = 0.0
|
||||
estimated_cost = 0.0
|
||||
|
||||
if plan.cost_metadata:
|
||||
tokens_used = plan.cost_metadata.total_tokens or 0
|
||||
cost_so_far = plan.cost_metadata.total_cost or 0.0
|
||||
|
||||
if plan.estimation_result and plan.estimation_result.estimated_cost_usd:
|
||||
estimated_cost = plan.estimation_result.estimated_cost_usd
|
||||
|
||||
cost_details = (
|
||||
f"[#5599ff bold]Tokens Used:[/#5599ff bold] {tokens_used:,}\n"
|
||||
f"[yellow bold]Cost So Far:[/yellow bold] ${cost_so_far:.3f}\n"
|
||||
f"[#5599ff bold]Estimated:[/#5599ff bold] ${estimated_cost:.3f}"
|
||||
)
|
||||
console.print(Panel(cost_details, title="Cost", expand=False))
|
||||
|
||||
# ===== Footer =====
|
||||
console.print("[#66cc66 bold]✓ OK[/#66cc66 bold] Status refreshed")
|
||||
|
||||
|
||||
@app.command("use")
|
||||
|
||||
Reference in New Issue
Block a user