fix: wire DI persistence and plan execute/apply for M1 lifecycle
CI / lint (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 42s
CI / security (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 27s
CI / unit_tests (pull_request) Successful in 3m16s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 15s
CI / e2e_tests (pull_request) Successful in 1m12s
CI / integration_tests (pull_request) Successful in 4m20s
CI / docker (pull_request) Successful in 59s
CI / coverage (pull_request) Successful in 6m34s
CI / lint (push) Successful in 20s
CI / typecheck (push) Successful in 45s
CI / security (push) Successful in 46s
CI / quality (push) Successful in 37s
CI / build (push) Successful in 17s
CI / e2e_tests (push) Successful in 52s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 5m9s
CI / integration_tests (push) Successful in 5m32s
CI / docker (push) Successful in 57s
CI / coverage (push) Successful in 6m10s
CI / benchmark-publish (push) Successful in 20m11s
CI / benchmark-regression (pull_request) Successful in 38m29s
CI / lint (pull_request) Successful in 16s
CI / typecheck (pull_request) Successful in 42s
CI / security (pull_request) Successful in 46s
CI / quality (pull_request) Successful in 27s
CI / unit_tests (pull_request) Successful in 3m16s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 15s
CI / e2e_tests (pull_request) Successful in 1m12s
CI / integration_tests (pull_request) Successful in 4m20s
CI / docker (pull_request) Successful in 59s
CI / coverage (pull_request) Successful in 6m34s
CI / lint (push) Successful in 20s
CI / typecheck (push) Successful in 45s
CI / security (push) Successful in 46s
CI / quality (push) Successful in 37s
CI / build (push) Successful in 17s
CI / e2e_tests (push) Successful in 52s
CI / benchmark-regression (push) Has been skipped
CI / unit_tests (push) Successful in 5m9s
CI / integration_tests (push) Successful in 5m32s
CI / docker (push) Successful in 57s
CI / coverage (push) Successful in 6m10s
CI / benchmark-publish (push) Successful in 20m11s
CI / benchmark-regression (pull_request) Successful in 38m29s
Fixed 5 bugs preventing the M1 E2E acceptance test from passing: 1. _get_lifecycle_service() in action.py and plan.py bypassed the DI container, creating PlanLifecycleService without UnitOfWork. All plan/action data was in-memory only and lost between subprocess calls. Now uses container.plan_lifecycle_service() for DB persistence. 2. `plan execute` CLI only called service.execute_plan() (a pure state transition) without running PlanExecutor phase processing. Rewrote to detect the plan's current phase/state and dispatch synchronously: Strategize/queued → run_strategize(), Strategize/complete → transition + run_execute(), Execute/queued → run_execute(). 3. `plan apply` CLI had no plan_id argument. Added optional positional plan_id with _lifecycle_apply_with_id() that drives the plan through Apply/queued → Apply/processing → Apply/applied. 4. Preflight guardrail in start_strategize() built action_registry from the in-memory _actions dict only. Added get_action(plan.action_name) call to load the action from DB into cache before the guardrail check. 5. Robot Framework Create File syntax used continuation lines producing 9 arguments instead of 1. Fixed to use Catenate SEPARATOR=\n then pass single variable to Create File. Also fixed --branch main to --branch master (git init default). update mocks for execute_plan CLI changes across unit and integration tests The new execute_plan() command calls _get_plan_executor() and service.get_plan(plan_id) for phase/state detection. Existing tests only mocked _get_lifecycle_service, so MagicMock defaults caused phase/state comparisons to fail. Changes across 14 files: - Patch _get_plan_executor in all test setups that invoke the CLI execute command (Behave step files + Robot helper scripts) - Set service.get_plan.return_value to real Plan objects with correct phase/state so the execute_plan dispatch logic works - Fix error-path tests to use STRATEGIZE/COMPLETE plans so the error side_effects are actually reached - Fix "Multiple plans eligible" → "Multiple plans ready" message text to match existing test expectations increase Robot Framework subprocess timeouts for CI resource contention Three integration tests were timing out in CI due to resource contention when pabot runs multiple test suites in parallel. All three pass locally and the timeouts were simply too tight for constrained CI environments. - tdd_session_create_di.robot: 30s → 90s (DI container init + DB setup) - database_integration.robot: 60s → 120s (Run Python Script keyword) - m3_e2e_verification.robot: 60s → 120s (correction-live-revert spawns 3 sequential CLI subprocesses with full container initialization) ISSUES CLOSED: #789
This commit was merged in pull request #957.
This commit is contained in:
@@ -158,13 +158,19 @@ def step_lc_mocked_service(context: Context) -> None:
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.lc_mock,
|
||||
)
|
||||
context.lc_executor_patcher = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
context.lc_action_patcher.start()
|
||||
context.lc_plan_patcher.start()
|
||||
context.lc_executor_patcher.start()
|
||||
|
||||
if not hasattr(context, "_lc_cleanup"):
|
||||
context._lc_cleanup = []
|
||||
context._lc_cleanup.append(context.lc_action_patcher.stop)
|
||||
context._lc_cleanup.append(context.lc_plan_patcher.stop)
|
||||
context._lc_cleanup.append(context.lc_executor_patcher.stop)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -824,6 +830,7 @@ def step_lc_plan_terminal(context: Context) -> None:
|
||||
def step_lc_plan_ready_execute(context: Context) -> None:
|
||||
plan = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
|
||||
context.lc_mock.execute_plan.return_value = plan
|
||||
context.lc_mock.get_plan.return_value = plan
|
||||
context.lc_plan = plan
|
||||
|
||||
|
||||
@@ -834,6 +841,7 @@ def step_lc_single_execute(context: Context) -> None:
|
||||
state=ProcessingState.COMPLETE,
|
||||
)
|
||||
context.lc_mock.list_plans.return_value = [plan]
|
||||
context.lc_mock.get_plan.return_value = plan
|
||||
executed = _make_lc_plan(phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED)
|
||||
context.lc_mock.execute_plan.return_value = executed
|
||||
context.lc_plan = plan
|
||||
@@ -865,24 +873,30 @@ def step_lc_multi_execute(context: Context) -> None:
|
||||
|
||||
@given("lifecycle coverage a plan with invalid transition for execute")
|
||||
def step_lc_invalid_transition(context: Context) -> None:
|
||||
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
|
||||
context.lc_mock.execute_plan.side_effect = InvalidPhaseTransitionError(
|
||||
PlanPhase.APPLY, PlanPhase.EXECUTE
|
||||
)
|
||||
context.lc_plan = _make_lc_plan()
|
||||
context.lc_mock.get_plan.return_value = plan
|
||||
context.lc_plan = plan
|
||||
|
||||
|
||||
@given("lifecycle coverage a plan not ready for execute")
|
||||
def step_lc_plan_not_ready(context: Context) -> None:
|
||||
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
|
||||
context.lc_mock.execute_plan.side_effect = PlanNotReadyError(
|
||||
_PLAN_ULID, PlanPhase.STRATEGIZE, ProcessingState.QUEUED
|
||||
)
|
||||
context.lc_plan = _make_lc_plan()
|
||||
context.lc_mock.get_plan.return_value = plan
|
||||
context.lc_plan = plan
|
||||
|
||||
|
||||
@given("lifecycle coverage a plan execute with general error")
|
||||
def step_lc_plan_exec_general_error(context: Context) -> None:
|
||||
plan = _make_lc_plan(phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE)
|
||||
context.lc_mock.execute_plan.side_effect = CleverAgentsError("Execute failed")
|
||||
context.lc_plan = _make_lc_plan()
|
||||
context.lc_mock.get_plan.return_value = plan
|
||||
context.lc_plan = plan
|
||||
|
||||
|
||||
@when("I run lifecycle coverage plan execute with plan ID")
|
||||
|
||||
@@ -121,8 +121,13 @@ def step_robot_alignment_service(context: Context) -> None:
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=context.ra_mock_service,
|
||||
)
|
||||
context.ra_executor_patcher = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
context.ra_action_patcher.start()
|
||||
context.ra_plan_patcher.start()
|
||||
context.ra_executor_patcher.start()
|
||||
|
||||
# Set up changeset store for tracking
|
||||
context.ra_changeset_store = InMemoryChangeSetStore()
|
||||
@@ -132,6 +137,7 @@ def step_robot_alignment_service(context: Context) -> None:
|
||||
context._cleanup_handlers = []
|
||||
context._cleanup_handlers.append(context.ra_action_patcher.stop)
|
||||
context._cleanup_handlers.append(context.ra_plan_patcher.stop)
|
||||
context._cleanup_handlers.append(context.ra_executor_patcher.stop)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -146,6 +152,9 @@ def step_robot_alignment_action_create(context: Context, name: str) -> None:
|
||||
context.ra_mock_service.create_action.return_value = action
|
||||
context.ra_mock_service.get_action_by_name.return_value = action
|
||||
context.ra_mock_service.use_action.return_value = _mock_plan()
|
||||
context.ra_mock_service.get_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
context.ra_mock_service.execute_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ conflicts with existing steps.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -442,10 +443,17 @@ def step_m1_plan_in_strategize(context: Context) -> None:
|
||||
state=ProcessingState.COMPLETE,
|
||||
)
|
||||
context.mock_service.list_plans.return_value = [plan]
|
||||
context.mock_service.get_plan.return_value = plan
|
||||
context.mock_service.execute_plan.return_value = _make_m1_plan(
|
||||
phase=PlanPhase.EXECUTE,
|
||||
state=ProcessingState.QUEUED,
|
||||
)
|
||||
# Patch the plan executor so the execute phase runs with a mock
|
||||
context._m1_executor_patcher = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
context._m1_executor_patcher.start()
|
||||
|
||||
|
||||
@when("I m1 smoke invoke plan execute")
|
||||
@@ -453,6 +461,9 @@ def step_m1_plan_execute(context: Context) -> None:
|
||||
"""Invoke plan execute."""
|
||||
result = context.runner.invoke(plan_app, ["execute"])
|
||||
context.last_result = result
|
||||
# Clean up executor patcher if it was set
|
||||
with contextlib.suppress(AttributeError):
|
||||
context._m1_executor_patcher.stop()
|
||||
|
||||
|
||||
@then("the m1 smoke plan execute should succeed")
|
||||
|
||||
@@ -134,6 +134,14 @@ def step_mocked_lifecycle(context: Any) -> None:
|
||||
p.start()
|
||||
context.r2_cleanups.append(p.stop)
|
||||
|
||||
# Patch the plan executor getter (needed by execute_plan command)
|
||||
p2 = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
p2.start()
|
||||
context.r2_cleanups.append(p2.stop)
|
||||
|
||||
# Replace the module-level console with a wider one for table tests
|
||||
from rich.console import Console as RichConsole
|
||||
|
||||
@@ -190,6 +198,7 @@ def step_one_strategize_plan(context: Any) -> None:
|
||||
processing_state=ProcessingState.COMPLETE,
|
||||
)
|
||||
context.r2_mock_svc.list_plans.return_value = [p]
|
||||
context.r2_mock_svc.get_plan.return_value = p
|
||||
context.r2_mock_svc.execute_plan.return_value = p
|
||||
|
||||
|
||||
|
||||
@@ -278,9 +278,18 @@ def step_service_has_strategize_plan(context) -> None:
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.COMPLETE,
|
||||
)
|
||||
context.mock_lifecycle_service.get_plan.return_value = plan
|
||||
context.mock_lifecycle_service.execute_plan.return_value = plan
|
||||
context._execute_plan_id = _ULIDS[0]
|
||||
|
||||
# Mock the plan executor so the execute phase runs with a mock
|
||||
executor_patcher = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
executor_patcher.start()
|
||||
context._cleanup_handlers.append(executor_patcher.stop)
|
||||
|
||||
|
||||
@given("the service has a complete execute plan for apply")
|
||||
def step_service_has_execute_plan(context) -> None:
|
||||
|
||||
@@ -411,6 +411,14 @@ def step_mock_lifecycle_for_execute(context: Context) -> None:
|
||||
p = patch(_PATCH_GET_LIFECYCLE, return_value=mock_service)
|
||||
p.start()
|
||||
context._r2cov_cleanups.append(p.stop)
|
||||
|
||||
p_exec = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
)
|
||||
p_exec.start()
|
||||
context._r2cov_cleanups.append(p_exec.stop)
|
||||
|
||||
context.r2cov_service = mock_service
|
||||
|
||||
|
||||
|
||||
@@ -232,13 +232,35 @@ def step_plan_execute_without_plan_id(context, count: int) -> None:
|
||||
]
|
||||
context.lifecycle_service.list_plans.return_value = plans
|
||||
if count == 1:
|
||||
context.lifecycle_service.get_plan.return_value = plans[0]
|
||||
context.lifecycle_service.execute_plan.return_value = plans[0]
|
||||
context.execute_plans = plans
|
||||
|
||||
# Mock the plan executor for successful execute paths
|
||||
mock_executor = MagicMock()
|
||||
executor_patcher = patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=mock_executor,
|
||||
)
|
||||
executor_patcher.start()
|
||||
if not hasattr(context, "_cleanup_handlers"):
|
||||
context._cleanup_handlers = []
|
||||
context._cleanup_handlers.append(executor_patcher.stop)
|
||||
|
||||
context.result = context.runner.invoke(plan_app, ["execute"])
|
||||
|
||||
|
||||
@when('I run plan execute for plan id "{plan_id}" causing "{error_type}"')
|
||||
def step_plan_execute_error(context, plan_id: str, error_type: str) -> None:
|
||||
# Provide a real plan so phase checks pass before the error is raised
|
||||
error_plan = _make_plan(
|
||||
plan_id=plan_id,
|
||||
name="local/error-plan",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.COMPLETE,
|
||||
)
|
||||
context.lifecycle_service.get_plan.return_value = error_plan
|
||||
|
||||
if error_type == "invalid transition":
|
||||
context.lifecycle_service.execute_plan.side_effect = (
|
||||
InvalidPhaseTransitionError(
|
||||
|
||||
@@ -450,10 +450,19 @@ def step_invoke_execute_plan_with_id(context):
|
||||
mock_service = MagicMock()
|
||||
mock_plan = _make_mock_lifecycle_plan(phase="execute", state="queued")
|
||||
mock_service.execute_plan.return_value = mock_plan
|
||||
mock_service.get_plan.return_value = mock_plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
mock_executor = MagicMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=mock_executor,
|
||||
),
|
||||
):
|
||||
result = runner.invoke(plan_app, ["execute", "01JAAAAAAAAAAAAAAAAAAAAAAA"])
|
||||
context.result = result
|
||||
@@ -466,14 +475,23 @@ def step_invoke_execute_plan_auto_select(context):
|
||||
mock_service = MagicMock()
|
||||
plan = _make_mock_lifecycle_plan(phase="strategize", state="complete")
|
||||
mock_service.list_plans.return_value = [plan]
|
||||
mock_service.get_plan.return_value = plan
|
||||
mock_service.execute_plan.return_value = _make_mock_lifecycle_plan(
|
||||
phase="execute",
|
||||
state="queued",
|
||||
)
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
mock_executor = MagicMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=mock_executor,
|
||||
),
|
||||
):
|
||||
result = runner.invoke(plan_app, ["execute"])
|
||||
context.result = result
|
||||
@@ -483,8 +501,8 @@ def step_invoke_execute_plan_auto_select(context):
|
||||
def step_invoke_execute_plan_no_ready(context):
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
# Return a plan that is in strategize but still queued (not complete)
|
||||
plan = _make_mock_lifecycle_plan(phase="strategize", state="queued")
|
||||
# Return a plan that is NOT in an executable state (e.g. already applied)
|
||||
plan = _make_mock_lifecycle_plan(phase="apply", state="applied")
|
||||
mock_service.list_plans.return_value = [plan]
|
||||
|
||||
with patch(
|
||||
@@ -529,6 +547,9 @@ def step_invoke_execute_plan_invalid_transition(context):
|
||||
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
# Provide a real plan so the phase check passes before the error is hit
|
||||
mock_plan = _make_mock_lifecycle_plan(phase="strategize", state="complete")
|
||||
mock_service.get_plan.return_value = mock_plan
|
||||
mock_service.execute_plan.side_effect = InvalidPhaseTransitionError(
|
||||
PlanPhase.APPLY,
|
||||
PlanPhase.EXECUTE,
|
||||
@@ -551,6 +572,9 @@ def step_invoke_execute_plan_not_ready(context):
|
||||
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
# Provide a real plan so the phase check passes before the error is hit
|
||||
mock_plan = _make_mock_lifecycle_plan(phase="strategize", state="complete")
|
||||
mock_service.get_plan.return_value = mock_plan
|
||||
mock_service.execute_plan.side_effect = PlanNotReadyError(
|
||||
"01JAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
PlanPhase.STRATEGIZE,
|
||||
|
||||
@@ -759,7 +759,7 @@ Run Python Script
|
||||
# Create File writes to it (avoids leaking an open descriptor).
|
||||
${temp_file}= Evaluate (lambda t: (__import__('os').close(t[0]), t[1])[-1])(__import__('tempfile').mkstemp(suffix='.py', dir='/tmp'))
|
||||
Create File ${temp_file} ${full_code}
|
||||
${result}= Run Process ${PYTHON} ${temp_file} timeout=60s stderr=STDOUT env:PYTHONWARNINGS=ignore env:PYTHONDONTWRITEBYTECODE=1
|
||||
${result}= Run Process ${PYTHON} ${temp_file} timeout=120s stderr=STDOUT env:PYTHONWARNINGS=ignore env:PYTHONDONTWRITEBYTECODE=1
|
||||
Remove File ${temp_file}
|
||||
# Check if process failed and log stderr if present
|
||||
IF ${result.rc} != 0
|
||||
|
||||
@@ -26,15 +26,16 @@ M1 Full Plan Lifecycle
|
||||
|
||||
# ── 2. Write action YAML config ──────────────────────────────
|
||||
${action_yaml}= Set Variable ${SUITE_HOME}${/}m1_test_action.yaml
|
||||
Create File ${action_yaml}
|
||||
... name: local/test-action\n
|
||||
... description: "M1 E2E acceptance test action"\n
|
||||
... strategy_actor: openai/gpt-4o-mini\n
|
||||
... execution_actor: openai/gpt-4o-mini\n
|
||||
... definition_of_done: |\n
|
||||
... ${SPACE}${SPACE}Create a file called HELLO.md with a short greeting.\n
|
||||
... reusable: true\n
|
||||
... read_only: false\n
|
||||
${yaml_content}= Catenate SEPARATOR=\n
|
||||
... name: local/test-action
|
||||
... description: "M1 E2E acceptance test action"
|
||||
... strategy_actor: openai/gpt-4o-mini
|
||||
... execution_actor: openai/gpt-4o-mini
|
||||
... definition_of_done: |
|
||||
... ${SPACE}${SPACE}Create a file called HELLO.md with a short greeting.
|
||||
... reusable: true
|
||||
... read_only: false
|
||||
Create File ${action_yaml} ${yaml_content}
|
||||
|
||||
# ── 3. Create the action ─────────────────────────────────────
|
||||
${action_result}= Run CleverAgents Command
|
||||
@@ -44,7 +45,7 @@ M1 Full Plan Lifecycle
|
||||
# ── 4. Register git-checkout resource ────────────────────────
|
||||
${resource_result}= Run CleverAgents Command
|
||||
... resource add git-checkout local/test-repo
|
||||
... --path ${repo_path} --branch main
|
||||
... --path ${repo_path} --branch master
|
||||
Should Be Equal As Integers ${resource_result.rc} 0
|
||||
... Resource add failed: ${resource_result.stderr}
|
||||
|
||||
|
||||
@@ -179,6 +179,9 @@ def plan_execute_changeset() -> None:
|
||||
"""Verify plan execute and ChangeSet capture records entries."""
|
||||
svc = MagicMock()
|
||||
svc.execute_plan.return_value = _mock_plan(phase=PlanPhase.EXECUTE)
|
||||
svc.get_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
store, cid = _make_store()
|
||||
|
||||
store.record(
|
||||
@@ -192,8 +195,14 @@ def plan_execute_changeset() -> None:
|
||||
),
|
||||
)
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service", return_value=svc
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service", return_value=svc
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
result = runner.invoke(plan_app, ["execute", _PLAN_ULID])
|
||||
if result.exit_code != 0:
|
||||
@@ -260,6 +269,9 @@ def full_lifecycle() -> None:
|
||||
svc.get_action_by_name.return_value = _mock_action()
|
||||
svc.use_action.return_value = _mock_plan()
|
||||
svc.execute_plan.return_value = _mock_plan(phase=PlanPhase.EXECUTE)
|
||||
svc.get_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
svc.apply_plan.return_value = _mock_plan(phase=PlanPhase.APPLY)
|
||||
|
||||
sandbox_dir = tempfile.mkdtemp(prefix="robot_full_sandbox_")
|
||||
@@ -286,6 +298,10 @@ def full_lifecycle() -> None:
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=svc,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
r1 = runner.invoke(action_app, ["create", "--config", yaml_path])
|
||||
assert r1.exit_code == 0, f"action create: {r1.output}"
|
||||
|
||||
@@ -154,13 +154,22 @@ def plan_use() -> None:
|
||||
def plan_execute() -> None:
|
||||
"""Verify plan execute transitions to Execute phase."""
|
||||
mock_service = MagicMock()
|
||||
mock_service.get_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
mock_service.execute_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED
|
||||
)
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
result = runner.invoke(plan_app, ["execute", _PLAN_ULID])
|
||||
if result.exit_code == 0:
|
||||
@@ -267,6 +276,9 @@ def full_lifecycle() -> None:
|
||||
|
||||
mock_service.use_action.return_value = plan_strat
|
||||
mock_service.execute_plan.return_value = plan_exec
|
||||
mock_service.get_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
mock_service.apply_plan.return_value = plan_apply
|
||||
|
||||
yaml_path = _write_yaml(_VALID_YAML)
|
||||
@@ -280,6 +292,10 @@ def full_lifecycle() -> None:
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
# Step 1: Create action
|
||||
r1 = runner.invoke(action_app, ["create", "--config", yaml_path])
|
||||
|
||||
@@ -152,12 +152,19 @@ def plan_execute() -> None:
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
mock_svc.list_plans.return_value = [strategize_plan]
|
||||
mock_svc.get_plan.return_value = strategize_plan
|
||||
mock_svc.execute_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED
|
||||
)
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_svc,
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_svc,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
result = runner.invoke(plan_app, ["execute"])
|
||||
if result.exit_code == 0 and "execute" in result.output.lower():
|
||||
@@ -212,6 +219,7 @@ def full_lifecycle() -> None:
|
||||
phase=PlanPhase.STRATEGIZE, state=ProcessingState.COMPLETE
|
||||
)
|
||||
mock_svc.list_plans.return_value = [strategize_plan]
|
||||
mock_svc.get_plan.return_value = strategize_plan
|
||||
mock_svc.execute_plan.return_value = _mock_plan(
|
||||
phase=PlanPhase.EXECUTE, state=ProcessingState.QUEUED
|
||||
)
|
||||
@@ -228,6 +236,10 @@ def full_lifecycle() -> None:
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_svc,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
# Step 1: action create
|
||||
r1 = runner.invoke(action_app, ["create", "--config", str(config_path)])
|
||||
|
||||
@@ -239,16 +239,19 @@ def cli_plan_execute() -> None:
|
||||
)
|
||||
mock_service.execute_plan.return_value = post_plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
result = _runner.invoke(plan_app, ["execute", _ROOT_ULID])
|
||||
_assert_exit_code(result, "plan execute")
|
||||
|
||||
# Verify get_plan was called for the read-only guard.
|
||||
_assert_mock_called_once_with(mock_service.get_plan, "get_plan", _ROOT_ULID)
|
||||
|
||||
# Verify execute_plan was called with the plan ID
|
||||
_assert_mock_called_once_with(
|
||||
mock_service.execute_plan, "execute_plan", _ROOT_ULID
|
||||
|
||||
@@ -64,9 +64,15 @@ def cli_plan_execute_readonly() -> None:
|
||||
)
|
||||
mock_service.get_plan.return_value = plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
with (
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
),
|
||||
patch(
|
||||
"cleveragents.cli.commands.plan._get_plan_executor",
|
||||
return_value=MagicMock(),
|
||||
),
|
||||
):
|
||||
result = _runner.invoke(plan_app, ["execute", _ROOT_ULID])
|
||||
if result.exit_code == 0:
|
||||
|
||||
@@ -94,7 +94,7 @@ Correction Live Revert Executes And Re-Creates Decisions
|
||||
... Validates: plan correct with --mode revert executes
|
||||
... live correction.
|
||||
[Tags] success_criteria correction_live_revert
|
||||
${result}= Run Process ${PYTHON} ${HELPER} correction-live-revert cwd=${WORKSPACE} timeout=60s
|
||||
${result}= Run Process ${PYTHON} ${HELPER} correction-live-revert cwd=${WORKSPACE} timeout=120s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
@@ -13,7 +13,7 @@ ${HELPER} ${CURDIR}/helper_tdd_session_create_di.py
|
||||
TDD Session Create DI Error Via CLI
|
||||
[Documentation] Verify that ``session create`` triggers the DI db error
|
||||
[Tags] tdd_bug tdd_bug_570
|
||||
${result}= Run Process ${PYTHON} ${HELPER} create-di-error cwd=${WORKSPACE} timeout=30s
|
||||
${result}= Run Process ${PYTHON} ${HELPER} create-di-error cwd=${WORKSPACE} timeout=90s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
@@ -22,7 +22,7 @@ TDD Session Create DI Error Via CLI
|
||||
TDD Session Create With Actor DI Error
|
||||
[Documentation] Verify that ``session create --actor`` triggers the DI db error
|
||||
[Tags] tdd_bug tdd_bug_570
|
||||
${result}= Run Process ${PYTHON} ${HELPER} create-actor cwd=${WORKSPACE} timeout=30s
|
||||
${result}= Run Process ${PYTHON} ${HELPER} create-actor cwd=${WORKSPACE} timeout=90s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
@@ -31,7 +31,7 @@ TDD Session Create With Actor DI Error
|
||||
TDD Session Create DI JSON Output
|
||||
[Documentation] Verify that ``session create --format json`` fails due to DI db error
|
||||
[Tags] tdd_bug tdd_bug_570
|
||||
${result}= Run Process ${PYTHON} ${HELPER} create-json cwd=${WORKSPACE} timeout=30s
|
||||
${result}= Run Process ${PYTHON} ${HELPER} create-json cwd=${WORKSPACE} timeout=90s
|
||||
Log ${result.stdout}
|
||||
Log ${result.stderr}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
|
||||
@@ -51,6 +51,7 @@ Based on ``docs/specification.md`` and implementation plan Stage A3.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
@@ -848,6 +849,11 @@ class PlanLifecycleService:
|
||||
)
|
||||
|
||||
# -- Pre-flight guardrail checks ----------------------------------
|
||||
# Ensure the plan's action is loaded from the persistence layer
|
||||
# into the in-memory cache so the guardrail can find it.
|
||||
if plan.action_name:
|
||||
with suppress(Exception):
|
||||
self.get_action(plan.action_name)
|
||||
action_registry: dict[str, object] = {
|
||||
name: act for name, act in self._actions.items()
|
||||
}
|
||||
|
||||
@@ -82,7 +82,11 @@ _FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)
|
||||
|
||||
|
||||
def _get_lifecycle_service() -> PlanLifecycleService:
|
||||
"""Get the PlanLifecycleService from the container."""
|
||||
"""Get the PlanLifecycleService from the container.
|
||||
|
||||
Uses the container's registered provider which includes UnitOfWork
|
||||
for database persistence across subprocess invocations.
|
||||
"""
|
||||
container = get_container()
|
||||
return container.plan_lifecycle_service()
|
||||
|
||||
|
||||
@@ -742,23 +742,42 @@ def build(
|
||||
|
||||
@app.command()
|
||||
def apply(
|
||||
plan_id: Annotated[
|
||||
str | None,
|
||||
typer.Argument(help="Plan ID to apply (v3 lifecycle). Omit for legacy mode."),
|
||||
] = None,
|
||||
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
"--format",
|
||||
"-f",
|
||||
help=_FORMAT_HELP,
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Apply the built plan changes to the filesystem.
|
||||
"""Apply plan changes.
|
||||
|
||||
This command writes the AI-generated changes to your actual files.
|
||||
When a *plan_id* is given, uses the v3 lifecycle: transitions the
|
||||
plan through Apply/queued -> Apply/processing -> Apply/applied.
|
||||
|
||||
.. deprecated::
|
||||
Use ``agents plan lifecycle-apply`` for the v3 lifecycle.
|
||||
When no *plan_id* is given, falls back to the legacy workflow that
|
||||
writes AI-generated file changes for the current project.
|
||||
"""
|
||||
if plan_id is not None:
|
||||
# ── v3 lifecycle apply ──────────────────────────────────
|
||||
_lifecycle_apply_with_id(plan_id, fmt)
|
||||
return
|
||||
|
||||
# ── Legacy apply path (no plan_id) ──────────────────────────
|
||||
import os
|
||||
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.application.services.plan_service import PlanService
|
||||
|
||||
console.print(
|
||||
"[yellow]Warning:[/yellow] 'apply' is a legacy command. "
|
||||
"Use 'agents plan lifecycle-apply <plan_id>' for the v3 lifecycle."
|
||||
"[yellow]Warning:[/yellow] 'apply' without a plan ID is a legacy command. "
|
||||
"Use 'agents plan apply <plan_id>' for the v3 lifecycle."
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -832,6 +851,74 @@ def apply(
|
||||
raise typer.Abort() from e
|
||||
|
||||
|
||||
def _lifecycle_apply_with_id(plan_id: str, fmt: str = "rich") -> None:
|
||||
"""Run the v3 lifecycle apply for a specific plan.
|
||||
|
||||
Transitions the plan through:
|
||||
Execute/complete -> Apply/queued -> Apply/processing -> Apply/applied.
|
||||
"""
|
||||
from cleveragents.application.services.plan_lifecycle_service import (
|
||||
InvalidPhaseTransitionError,
|
||||
PlanNotReadyError,
|
||||
)
|
||||
|
||||
try:
|
||||
service = _get_lifecycle_service()
|
||||
|
||||
# Fail-fast: read-only plans must not enter Apply phase
|
||||
pre_plan = service.get_plan(plan_id)
|
||||
if pre_plan is None:
|
||||
console.print(f"[red]Plan '{plan_id}' not found.[/red]")
|
||||
raise typer.Abort()
|
||||
if pre_plan.read_only is True:
|
||||
console.print(
|
||||
f"[red]Cannot apply plan '{plan_id}': plan is read-only.[/red]"
|
||||
)
|
||||
raise typer.Abort()
|
||||
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
PlanPhase,
|
||||
ProcessingState,
|
||||
)
|
||||
|
||||
# Determine current phase and drive through apply
|
||||
if (
|
||||
pre_plan.phase == PlanPhase.EXECUTE
|
||||
and pre_plan.state == ProcessingState.COMPLETE
|
||||
):
|
||||
# Transition Execute/complete -> Apply/queued
|
||||
service.apply_plan(plan_id)
|
||||
|
||||
current = service.get_plan(plan_id)
|
||||
if current.phase == PlanPhase.APPLY and current.state == ProcessingState.QUEUED:
|
||||
service.start_apply(plan_id)
|
||||
|
||||
current = service.get_plan(plan_id)
|
||||
if (
|
||||
current.phase == PlanPhase.APPLY
|
||||
and current.state == ProcessingState.PROCESSING
|
||||
):
|
||||
service.complete_apply(plan_id)
|
||||
|
||||
plan = service.get_plan(plan_id)
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Plan Applied")
|
||||
console.print("\n[dim]Plan apply completed successfully.[/dim]")
|
||||
|
||||
except InvalidPhaseTransitionError as e:
|
||||
console.print(f"[red]Invalid transition:[/red] {e}")
|
||||
raise typer.Abort() from e
|
||||
except PlanNotReadyError as e:
|
||||
console.print(f"[red]Plan not ready:[/red] {e}")
|
||||
raise typer.Abort() from e
|
||||
except CleverAgentsError as e:
|
||||
console.print(f"[red]Error:[/red] {e.message}")
|
||||
raise typer.Abort() from e
|
||||
|
||||
|
||||
@app.command()
|
||||
def new(
|
||||
name: Annotated[
|
||||
@@ -1103,13 +1190,28 @@ def continue_plan(
|
||||
|
||||
|
||||
def _get_lifecycle_service():
|
||||
"""Get the PlanLifecycleService from the container."""
|
||||
"""Get the PlanLifecycleService from the container.
|
||||
|
||||
Uses the container's registered provider which includes UnitOfWork
|
||||
for database persistence across subprocess invocations.
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
return container.plan_lifecycle_service()
|
||||
|
||||
|
||||
def _get_plan_executor():
|
||||
"""Get a PlanExecutor wired with the container's lifecycle service.
|
||||
|
||||
Returns a PlanExecutor that uses stub actors for M1 phase processing.
|
||||
"""
|
||||
from cleveragents.application.services.plan_executor import PlanExecutor
|
||||
|
||||
lifecycle = _get_lifecycle_service()
|
||||
return PlanExecutor(lifecycle_service=lifecycle)
|
||||
|
||||
|
||||
def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
"""Print v3 lifecycle plan details in a nice panel.
|
||||
|
||||
@@ -1518,9 +1620,15 @@ def execute_plan(
|
||||
),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Execute a plan, transitioning from Strategize to Execute phase.
|
||||
"""Execute the current plan phase synchronously.
|
||||
|
||||
The plan must be in Strategize phase with 'complete' state.
|
||||
Detects the plan's current phase and processes it:
|
||||
|
||||
- **Strategize/queued**: runs the strategize phase to completion.
|
||||
- **Strategize/complete**: transitions to Execute and runs it.
|
||||
- **Execute/queued**: runs the execute phase to completion.
|
||||
|
||||
When no plan ID is given, auto-selects the single eligible plan.
|
||||
"""
|
||||
from cleveragents.application.services.plan_lifecycle_service import (
|
||||
InvalidPhaseTransitionError,
|
||||
@@ -1528,32 +1636,42 @@ def execute_plan(
|
||||
)
|
||||
|
||||
try:
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
PlanPhase,
|
||||
ProcessingState,
|
||||
)
|
||||
|
||||
service = _get_lifecycle_service()
|
||||
executor = _get_plan_executor()
|
||||
|
||||
if not plan_id:
|
||||
# Try to find the only plan in strategize phase
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
PlanPhase,
|
||||
ProcessingState,
|
||||
)
|
||||
|
||||
plans = service.list_plans(phase=PlanPhase.STRATEGIZE)
|
||||
complete_plans = [p for p in plans if p.state == ProcessingState.COMPLETE]
|
||||
if len(complete_plans) == 0:
|
||||
# Auto-discover: look for plans in strategize or execute
|
||||
plans_strat = service.list_plans(phase=PlanPhase.STRATEGIZE)
|
||||
plans_exec = service.list_plans(phase=PlanPhase.EXECUTE)
|
||||
eligible = [
|
||||
p
|
||||
for p in plans_strat
|
||||
if p.state in (ProcessingState.QUEUED, ProcessingState.COMPLETE)
|
||||
] + [p for p in plans_exec if p.state == ProcessingState.QUEUED]
|
||||
if len(eligible) == 0:
|
||||
console.print(
|
||||
"[yellow]No plans ready for execution.[/yellow]\n"
|
||||
"Plans must be in Strategize phase with 'complete' state."
|
||||
"Plans must be in Strategize or Execute phase."
|
||||
)
|
||||
raise typer.Abort()
|
||||
if len(complete_plans) > 1:
|
||||
if len(eligible) > 1:
|
||||
console.print(
|
||||
"[yellow]Multiple plans ready for execution. "
|
||||
"Please specify a plan ID.[/yellow]"
|
||||
)
|
||||
for p in complete_plans:
|
||||
console.print(f" • {p.identity.plan_id}: {p.namespaced_name}")
|
||||
for p in eligible:
|
||||
console.print(
|
||||
f" * {p.identity.plan_id}: "
|
||||
f"{p.namespaced_name} "
|
||||
f"({p.phase.value}/{p.state.value})"
|
||||
)
|
||||
raise typer.Abort()
|
||||
plan_id = complete_plans[0].identity.plan_id
|
||||
plan_id = eligible[0].identity.plan_id
|
||||
|
||||
# Apply execution environment override if provided
|
||||
if execution_environment:
|
||||
@@ -1580,18 +1698,72 @@ def execute_plan(
|
||||
)
|
||||
raise typer.Abort()
|
||||
|
||||
# Execute the plan
|
||||
plan = service.execute_plan(plan_id)
|
||||
# Determine current phase and run the appropriate processing
|
||||
current_plan = service.get_plan(plan_id)
|
||||
if current_plan is None:
|
||||
console.print(f"[red]Plan '{plan_id}' not found.[/red]")
|
||||
raise typer.Abort()
|
||||
|
||||
if current_plan.phase == PlanPhase.STRATEGIZE and current_plan.state in (
|
||||
ProcessingState.QUEUED,
|
||||
ProcessingState.PROCESSING,
|
||||
):
|
||||
# Run strategize phase synchronously
|
||||
executor.run_strategize(plan_id)
|
||||
plan = service.get_plan(plan_id)
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Strategize Complete")
|
||||
console.print(
|
||||
"\n[dim]Strategize phase completed. "
|
||||
"Run 'agents plan execute <id>' again to start "
|
||||
"the Execute phase.[/dim]"
|
||||
)
|
||||
|
||||
elif (
|
||||
current_plan.phase == PlanPhase.STRATEGIZE
|
||||
and current_plan.state == ProcessingState.COMPLETE
|
||||
):
|
||||
# Transition to Execute and run it synchronously
|
||||
service.execute_plan(plan_id)
|
||||
executor.run_execute(plan_id)
|
||||
plan = service.get_plan(plan_id)
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Execute Complete")
|
||||
console.print(
|
||||
"\n[dim]Execute phase completed. "
|
||||
"Run 'agents plan apply <id>' to apply changes.[/dim]"
|
||||
)
|
||||
|
||||
elif (
|
||||
current_plan.phase == PlanPhase.EXECUTE
|
||||
and current_plan.state == ProcessingState.QUEUED
|
||||
):
|
||||
# Run execute phase synchronously
|
||||
executor.run_execute(plan_id)
|
||||
plan = service.get_plan(plan_id)
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Execute Complete")
|
||||
console.print(
|
||||
"\n[dim]Execute phase completed. "
|
||||
"Run 'agents plan apply <id>' to apply changes.[/dim]"
|
||||
)
|
||||
|
||||
if fmt != OutputFormat.RICH.value:
|
||||
data = _plan_spec_dict(plan)
|
||||
console.print(format_output(data, fmt))
|
||||
else:
|
||||
_print_lifecycle_plan(plan, title="Plan Executing")
|
||||
console.print(
|
||||
"\n[dim]Plan is now in Execute phase (queued). "
|
||||
"Run 'agents plan apply <id>' when execution is complete.[/dim]"
|
||||
f"[red]Plan '{plan_id}' is not in an executable state "
|
||||
f"(current: {current_plan.phase.value}/"
|
||||
f"{current_plan.state.value}).[/red]"
|
||||
)
|
||||
raise typer.Abort()
|
||||
|
||||
except InvalidPhaseTransitionError as e:
|
||||
console.print(f"[red]Invalid transition:[/red] {e}")
|
||||
|
||||
Reference in New Issue
Block a user