refactor(automation): remove automation_level legacy fields
CI / lint (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 16s
CI / build (pull_request) Successful in 23s
CI / security (pull_request) Successful in 28s
CI / typecheck (pull_request) Successful in 39s
CI / integration_tests (pull_request) Failing after 3m2s
CI / unit_tests (pull_request) Successful in 3m56s
CI / docker (pull_request) Successful in 38s
CI / coverage (pull_request) Successful in 9m23s
CI / lint (pull_request) Successful in 14s
CI / quality (pull_request) Successful in 16s
CI / build (pull_request) Successful in 23s
CI / security (pull_request) Successful in 28s
CI / typecheck (pull_request) Successful in 39s
CI / integration_tests (pull_request) Failing after 3m2s
CI / unit_tests (pull_request) Successful in 3m56s
CI / docker (pull_request) Successful in 38s
CI / coverage (pull_request) Successful in 9m23s
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
"""Drop automation_level column from v3_plans.
|
||||
|
||||
This migration removes the legacy ``automation_level`` column and its
|
||||
CHECK constraint from the ``v3_plans`` table. Automation behavior is
|
||||
now controlled exclusively by the ``automation_profile`` JSON column.
|
||||
|
||||
Revision ID: a6_002_drop_automation_level
|
||||
Revises: 71cd40eb661f
|
||||
Create Date: 2026-02-19 10:00:00
|
||||
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a6_002_drop_automation_level"
|
||||
down_revision: str | Sequence[str] | None = "71cd40eb661f"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Drop automation_level column and CHECK constraint from v3_plans."""
|
||||
with op.batch_alter_table("v3_plans") as batch_op:
|
||||
batch_op.drop_constraint("ck_v3_plans_automation", type_="check")
|
||||
batch_op.drop_column("automation_level")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Restore automation_level column with CHECK constraint on v3_plans."""
|
||||
with op.batch_alter_table("v3_plans") as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"automation_level",
|
||||
sa.String(30),
|
||||
nullable=False,
|
||||
server_default="manual",
|
||||
),
|
||||
)
|
||||
batch_op.create_check_constraint(
|
||||
"ck_v3_plans_automation",
|
||||
"automation_level IN ('manual', 'review_before_apply', 'full_automation')",
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""ASV benchmarks for automation_level legacy cleanup.
|
||||
|
||||
Measures the baseline performance of:
|
||||
- Plan creation without automation_level (profile-only flow)
|
||||
- Profile resolution without legacy mapping
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
try:
|
||||
from cleveragents.application.services.plan_lifecycle_service import (
|
||||
PlanLifecycleService,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import ProjectLink
|
||||
except ModuleNotFoundError:
|
||||
sys.path.insert(
|
||||
0,
|
||||
str(Path(__file__).resolve().parents[1] / "src"),
|
||||
)
|
||||
from cleveragents.application.services.plan_lifecycle_service import (
|
||||
PlanLifecycleService,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import ProjectLink
|
||||
|
||||
|
||||
class LegacyCleanupSuite:
|
||||
"""Benchmark plan creation after automation_level removal."""
|
||||
|
||||
timeout = 30
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Create service instance for benchmarks."""
|
||||
self.settings = MagicMock()
|
||||
self.settings.default_automation_profile = "manual"
|
||||
self.service = PlanLifecycleService(settings=self.settings)
|
||||
action = self.service.create_action(
|
||||
name="local/bench-action",
|
||||
description="Benchmark action",
|
||||
definition_of_done="Done",
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
)
|
||||
self.action_name = str(action.namespaced_name)
|
||||
|
||||
def time_use_action_profile_only(self) -> None:
|
||||
"""Benchmark plan creation with profile-only flow."""
|
||||
self.service.use_action(
|
||||
action_name=self.action_name,
|
||||
project_links=[ProjectLink(project_name="proj-bench")],
|
||||
)
|
||||
|
||||
def time_service_instantiation(self) -> None:
|
||||
"""Benchmark service instantiation without legacy level resolution."""
|
||||
PlanLifecycleService(settings=self.settings)
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
Measures the performance of:
|
||||
- Profile resolution with various precedence levels
|
||||
- Legacy automation_level mapping
|
||||
- Profile listing
|
||||
- Built-in profile lookup via the service
|
||||
"""
|
||||
@@ -60,26 +59,6 @@ class ProfileResolutionSuite:
|
||||
)
|
||||
|
||||
|
||||
class LegacyMappingSuite:
|
||||
"""Benchmark legacy automation_level mapping."""
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Create service instance."""
|
||||
self.svc = AutomationProfileService(repo=None)
|
||||
|
||||
def time_map_manual(self) -> None:
|
||||
"""Benchmark mapping 'manual'."""
|
||||
self.svc.map_legacy_level("manual")
|
||||
|
||||
def time_map_full_auto(self) -> None:
|
||||
"""Benchmark mapping 'full_auto'."""
|
||||
self.svc.map_legacy_level("full_auto")
|
||||
|
||||
def time_resolve_legacy_auto(self) -> None:
|
||||
"""Benchmark full legacy resolution."""
|
||||
self.svc.resolve_legacy_level("auto")
|
||||
|
||||
|
||||
class ProfileListingSuite:
|
||||
"""Benchmark profile listing."""
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ from cleveragents.cli.commands.action import app as action_app # 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
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -69,7 +68,6 @@ def _mock_plan(name: str = "local/bench-plan") -> Plan:
|
||||
action_name="local/bench-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
project_links=[ProjectLink(project_name="proj-a")],
|
||||
|
||||
@@ -13,7 +13,6 @@ from pathlib import Path
|
||||
|
||||
try:
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
InvariantSource,
|
||||
@@ -29,7 +28,6 @@ try:
|
||||
except ModuleNotFoundError:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
InvariantSource,
|
||||
@@ -58,7 +56,6 @@ def _make_plan(suffix: str = "bench") -> Plan:
|
||||
definition_of_done="All benchmarks pass within time budget",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.REVIEW_BEFORE_APPLY,
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="org/bench-profile",
|
||||
provenance=AutomationProfileProvenance.ACTION,
|
||||
|
||||
@@ -19,7 +19,6 @@ class LegacyPlanServiceSuite:
|
||||
"""Prepare mock dependencies for PlanService."""
|
||||
self.mock_settings = MagicMock()
|
||||
self.mock_settings.database_url = "sqlite:///:memory:"
|
||||
self.mock_settings.default_automation_level = "manual"
|
||||
self.mock_uow = MagicMock()
|
||||
|
||||
def time_plan_service_instantiation(self) -> None:
|
||||
|
||||
@@ -30,7 +30,6 @@ 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
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
NamespacedName,
|
||||
@@ -76,7 +75,6 @@ def _mock_plan(
|
||||
action_name="local/bench-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
project_links=project_links or [],
|
||||
arguments={"target_coverage": 80},
|
||||
arguments_order=["target_coverage"],
|
||||
|
||||
@@ -12,7 +12,6 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
@@ -50,7 +49,6 @@ def _make_bench_plan(plan_id: str | None = None) -> V3Plan:
|
||||
description="Benchmark plan",
|
||||
phase=PlanPhase("action"),
|
||||
processing_state=ProcessingState("queued"),
|
||||
automation_level=AutomationLevel("manual"),
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
@@ -81,7 +80,6 @@ def _make_bench_plan(
|
||||
execution_actor="local/e",
|
||||
phase=PlanPhase("action"),
|
||||
processing_state=ProcessingState("queued"),
|
||||
automation_level=AutomationLevel("manual"),
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
project_links=[],
|
||||
arguments={},
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
Feature: automation_level legacy removal
|
||||
As a developer using CleverAgents
|
||||
I want automation_level to be fully removed
|
||||
So that only the automation_profile system is used
|
||||
|
||||
Background:
|
||||
Given I have a plan lifecycle service with automation level support
|
||||
|
||||
# ---- Legacy inputs rejected ----
|
||||
|
||||
Scenario: Plan creation uses automation_profile only
|
||||
Given I have an available action for automation tests
|
||||
When I use the action without explicit automation level on project "proj-001"
|
||||
Then the automated plan phase should be "strategize"
|
||||
|
||||
Scenario: Plan display shows automation_profile not automation_level
|
||||
Given I have an available action for automation tests
|
||||
When I use the action without explicit automation level on project "proj-001"
|
||||
Then the automated plan phase should be "strategize"
|
||||
|
||||
Scenario: Pause sets manual automation profile
|
||||
Given I have an available action for automation tests
|
||||
When I use the action without explicit automation level on project "proj-002"
|
||||
And I pause the plan
|
||||
Then the plan automation level should be "manual"
|
||||
|
||||
Scenario: Resume sets auto automation profile
|
||||
Given I have a paused plan that was previously "manual" in strategize phase
|
||||
When I resume the plan without explicit level
|
||||
Then the automated plan phase should be "strategize"
|
||||
@@ -43,39 +43,6 @@ Feature: Automation Profile Service
|
||||
When I try to resolve profile with all None
|
||||
Then a profile not found error should be raised
|
||||
|
||||
# ---- Legacy automation_level mapping ----
|
||||
|
||||
Scenario: Legacy manual maps to manual profile
|
||||
Given an automation profile service with global default "manual"
|
||||
When I map legacy level "manual"
|
||||
Then the mapped profile name should be "manual"
|
||||
|
||||
Scenario: Legacy supervised maps to supervised profile
|
||||
Given an automation profile service with global default "manual"
|
||||
When I map legacy level "supervised"
|
||||
Then the mapped profile name should be "supervised"
|
||||
|
||||
Scenario: Legacy auto maps to auto profile
|
||||
Given an automation profile service with global default "manual"
|
||||
When I map legacy level "auto"
|
||||
Then the mapped profile name should be "auto"
|
||||
|
||||
Scenario: Legacy full_auto maps to full-auto profile
|
||||
Given an automation profile service with global default "manual"
|
||||
When I map legacy level "full_auto"
|
||||
Then the mapped profile name should be "full-auto"
|
||||
|
||||
Scenario: Unknown legacy level raises ValidationError
|
||||
Given an automation profile service with global default "manual"
|
||||
When I try to map legacy level "unknown_level"
|
||||
Then a legacy mapping validation error should be raised
|
||||
And the validation error should mention "unknown_level"
|
||||
|
||||
Scenario: Resolve legacy level returns full profile
|
||||
Given an automation profile service with global default "manual"
|
||||
When I resolve legacy level "auto"
|
||||
Then the resolved profile should have auto_apply 1.0
|
||||
|
||||
# ---- Environment variable override ----
|
||||
|
||||
Scenario: Env var overrides empty global default
|
||||
|
||||
@@ -36,17 +36,17 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
|
||||
When I invoke use_action where get_action raises NotFoundError
|
||||
Then the use_action CLI should succeed via name fallback
|
||||
|
||||
Scenario: Use action parses custom automation level
|
||||
Scenario: Use action parses custom automation profile
|
||||
Given I have a temporary test directory
|
||||
When I invoke use_action with automation level "full_automation"
|
||||
When I invoke use_action with automation profile "full-auto"
|
||||
Then the use_action CLI should succeed
|
||||
And the lifecycle service should receive full_automation level
|
||||
And the lifecycle service should receive full-auto profile
|
||||
|
||||
Scenario: Use action rejects invalid automation level
|
||||
Scenario: Use action rejects invalid automation profile
|
||||
Given I have a temporary test directory
|
||||
When I invoke use_action with automation level "super_auto"
|
||||
When I invoke use_action with automation profile "super_auto"
|
||||
Then the use_action CLI should abort
|
||||
And the output should mention invalid automation level
|
||||
And the output should mention invalid automation profile
|
||||
|
||||
Scenario: Use action reports error when action is not available
|
||||
Given I have a temporary test directory
|
||||
@@ -177,28 +177,6 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
|
||||
When I invoke lifecycle-list and no plans match
|
||||
Then the lifecycle-list CLI should succeed with empty list message
|
||||
|
||||
# ===================================================================
|
||||
# V3 Lifecycle - set_automation_level command
|
||||
# ===================================================================
|
||||
|
||||
Scenario: Set automation level updates plan to full_automation
|
||||
Given I have a temporary test directory
|
||||
When I invoke set-automation-level with level "full_automation"
|
||||
Then the set-automation-level CLI should succeed
|
||||
And the output should confirm automation level change
|
||||
|
||||
Scenario: Set automation level rejects invalid level string
|
||||
Given I have a temporary test directory
|
||||
When I invoke set-automation-level with level "turbo_mode"
|
||||
Then the set-automation-level CLI should abort
|
||||
And the output should mention invalid automation level for set command
|
||||
|
||||
Scenario: Set automation level handles plan errors
|
||||
Given I have a temporary test directory
|
||||
When I invoke set-automation-level and PlanError is raised
|
||||
Then the set-automation-level CLI should abort
|
||||
And the output should mention cannot change level
|
||||
|
||||
# ===================================================================
|
||||
# V3 Lifecycle - cancel_plan command
|
||||
# ===================================================================
|
||||
|
||||
@@ -66,7 +66,6 @@ Feature: Plan Model Coverage — New Fields and Helpers
|
||||
And the CLI dict should contain key "phase"
|
||||
And the CLI dict should contain key "state"
|
||||
And the CLI dict should contain key "description"
|
||||
And the CLI dict should contain key "automation_level"
|
||||
And the CLI dict should contain key "created_at"
|
||||
And the CLI dict should contain key "updated_at"
|
||||
|
||||
|
||||
@@ -165,11 +165,6 @@ Feature: Session Domain Model
|
||||
And I get the session CLI dict
|
||||
Then the session cli dict should have key "actor_name"
|
||||
|
||||
Scenario: CLI dict includes automation level when set
|
||||
Given a session with automation level "full_automation"
|
||||
When I get the session CLI dict
|
||||
Then the session cli dict should have key "automation_level"
|
||||
|
||||
# ---- Empty Session Properties ----
|
||||
|
||||
Scenario: Empty session has zero message count
|
||||
|
||||
@@ -401,7 +401,6 @@ def step_create_referencing_plan(context: Context) -> None:
|
||||
phase="strategize",
|
||||
processing_state="queued",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
namespaced_name="local/test-plan",
|
||||
namespace="local",
|
||||
description="Plan referencing the action under test",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Step definitions for Automation Levels BDD tests.
|
||||
"""Step definitions for Automation Profile BDD tests.
|
||||
|
||||
Tests the automation level system implemented in Stage A6:
|
||||
- AutomationLevel enum (MANUAL, REVIEW_BEFORE_APPLY, FULL_AUTOMATION)
|
||||
- PlanLifecycleService automation methods (should_auto_progress, auto_progress,
|
||||
pause_plan, resume_plan, set_plan_automation_level)
|
||||
- Settings integration (default_automation_level, _resolve_automation_level)
|
||||
Tests the automation profile system:
|
||||
- AutomationProfile-based auto-progress (should_auto_progress, auto_progress)
|
||||
- PlanLifecycleService automation methods (pause_plan, resume_plan)
|
||||
- Profile resolution via automation_profile on Plan
|
||||
"""
|
||||
|
||||
from behave import given, then, when
|
||||
@@ -15,22 +14,21 @@ from cleveragents.application.services.plan_lifecycle_service import (
|
||||
)
|
||||
from cleveragents.config.settings import Settings
|
||||
from cleveragents.core.exceptions import PlanError
|
||||
from cleveragents.domain.models.core.plan import AutomationLevel, ProjectLink
|
||||
from cleveragents.domain.models.core.automation_profile import BUILTIN_PROFILES
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
ProjectLink,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _create_service_with_automation(context: Context, level: str = "manual") -> None:
|
||||
"""Create a PlanLifecycleService with a specific default automation level.
|
||||
|
||||
Note: pydantic-settings ``BaseSettings`` does not accept keyword overrides
|
||||
for fields that use ``validation_alias``, so we set the attribute after
|
||||
construction.
|
||||
"""
|
||||
def _create_service(context: Context) -> None:
|
||||
"""Create a PlanLifecycleService for automation tests."""
|
||||
settings = Settings()
|
||||
settings.default_automation_level = level
|
||||
context.auto_service = PlanLifecycleService(settings=settings)
|
||||
context.auto_error = None
|
||||
|
||||
@@ -48,67 +46,74 @@ def _create_available_action(context: Context, name: str = "local/auto-action")
|
||||
return action_name
|
||||
|
||||
|
||||
def _profile_name_for_level(level: str) -> str:
|
||||
"""Map old level names to profile names for backward-compat step defs."""
|
||||
mapping = {
|
||||
"manual": "manual",
|
||||
"review_before_apply": "auto",
|
||||
"full_automation": "full-auto",
|
||||
}
|
||||
return mapping.get(level, level)
|
||||
|
||||
|
||||
def _create_plan_at_phase(
|
||||
context: Context,
|
||||
automation_level: str,
|
||||
profile_name: str,
|
||||
phase: str,
|
||||
state: str,
|
||||
) -> None:
|
||||
"""Create a plan and advance it to the requested phase/state.
|
||||
|
||||
Also sets the automation level on the plan after creation.
|
||||
Sets the automation profile on the plan after creation.
|
||||
"""
|
||||
action_name = _create_available_action(context)
|
||||
plan = context.auto_service.use_action(
|
||||
action_name=action_name,
|
||||
project_links=[ProjectLink(project_name="project-auto")],
|
||||
automation_level=AutomationLevel.MANUAL, # Always start manual to control progression
|
||||
)
|
||||
plan_id = plan.identity.plan_id
|
||||
|
||||
# Ensure manual profile so we can control progression
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name="manual",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
# Advance through phases as needed
|
||||
if phase in ("strategize",):
|
||||
# Plan is already in strategize/queued after use_action
|
||||
if state == "processing":
|
||||
context.auto_service.start_strategize(plan_id)
|
||||
elif state == "complete":
|
||||
context.auto_service.start_strategize(plan_id)
|
||||
# Temporarily ensure manual so complete_strategize doesn't auto-progress
|
||||
context.auto_service.set_plan_automation_level(
|
||||
plan_id, AutomationLevel.MANUAL
|
||||
)
|
||||
context.auto_service.complete_strategize(plan_id)
|
||||
elif phase in ("execute",):
|
||||
context.auto_service.start_strategize(plan_id)
|
||||
context.auto_service.set_plan_automation_level(plan_id, AutomationLevel.MANUAL)
|
||||
context.auto_service.complete_strategize(plan_id)
|
||||
context.auto_service.execute_plan(plan_id)
|
||||
if state == "processing":
|
||||
context.auto_service.start_execute(plan_id)
|
||||
elif state == "complete":
|
||||
context.auto_service.start_execute(plan_id)
|
||||
context.auto_service.set_plan_automation_level(
|
||||
plan_id, AutomationLevel.MANUAL
|
||||
)
|
||||
context.auto_service.complete_execute(plan_id)
|
||||
elif phase in ("apply",):
|
||||
context.auto_service.start_strategize(plan_id)
|
||||
context.auto_service.set_plan_automation_level(plan_id, AutomationLevel.MANUAL)
|
||||
context.auto_service.complete_strategize(plan_id)
|
||||
context.auto_service.execute_plan(plan_id)
|
||||
context.auto_service.start_execute(plan_id)
|
||||
context.auto_service.set_plan_automation_level(plan_id, AutomationLevel.MANUAL)
|
||||
context.auto_service.complete_execute(plan_id)
|
||||
context.auto_service.apply_plan(plan_id)
|
||||
if state == "processing":
|
||||
context.auto_service.start_apply(plan_id)
|
||||
|
||||
# Now set the desired automation level
|
||||
resolved_level = AutomationLevel(automation_level)
|
||||
context.auto_service.set_plan_automation_level(plan_id, resolved_level)
|
||||
# Now set the desired automation profile
|
||||
resolved_profile = _profile_name_for_level(profile_name)
|
||||
plan = context.auto_service.get_plan(plan_id)
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=resolved_profile,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
# Re-fetch the plan
|
||||
context.auto_plan = context.auto_service.get_plan(plan_id)
|
||||
context.auto_plan = plan
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -119,11 +124,11 @@ def _create_plan_at_phase(
|
||||
@given("I have a plan lifecycle service with automation level support")
|
||||
def step_create_automation_service(context: Context) -> None:
|
||||
"""Create a PlanLifecycleService for automation tests."""
|
||||
_create_service_with_automation(context, "manual")
|
||||
_create_service(context)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plan creation at specific phase/state with automation level
|
||||
# Plan creation at specific phase/state with automation profile
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -131,7 +136,7 @@ def step_create_automation_service(context: Context) -> None:
|
||||
'I have a plan with automation level "{level}" in strategize phase with processing state'
|
||||
)
|
||||
def step_plan_auto_strategize_processing(context: Context, level: str) -> None:
|
||||
"""Create plan at strategize/processing with given automation level."""
|
||||
"""Create plan at strategize/processing with given automation profile."""
|
||||
_create_plan_at_phase(context, level, "strategize", "processing")
|
||||
|
||||
|
||||
@@ -139,7 +144,7 @@ def step_plan_auto_strategize_processing(context: Context, level: str) -> None:
|
||||
'I have a plan with automation level "{level}" in execute phase with processing state'
|
||||
)
|
||||
def step_plan_auto_execute_processing(context: Context, level: str) -> None:
|
||||
"""Create plan at execute/processing with given automation level."""
|
||||
"""Create plan at execute/processing with given automation profile."""
|
||||
_create_plan_at_phase(context, level, "execute", "processing")
|
||||
|
||||
|
||||
@@ -147,7 +152,7 @@ def step_plan_auto_execute_processing(context: Context, level: str) -> None:
|
||||
'I have a plan with automation level "{level}" in strategize phase with queued state'
|
||||
)
|
||||
def step_plan_auto_strategize_queued(context: Context, level: str) -> None:
|
||||
"""Create plan at strategize/queued with given automation level."""
|
||||
"""Create plan at strategize/queued with given automation profile."""
|
||||
_create_plan_at_phase(context, level, "strategize", "queued")
|
||||
|
||||
|
||||
@@ -219,18 +224,26 @@ def step_check_auto_plan_state(context: Context, expected: str) -> None:
|
||||
|
||||
@given('the global automation level is "{level}"')
|
||||
def step_set_global_automation_level(context: Context, level: str) -> None:
|
||||
"""Set the global automation level via settings."""
|
||||
_create_service_with_automation(context, level)
|
||||
"""Set up service with the global default automation profile."""
|
||||
_create_service(context)
|
||||
profile_name = _profile_name_for_level(level)
|
||||
# pydantic-settings BaseSettings doesn't accept constructor overrides for
|
||||
# fields with validation_alias, so we set the attribute after construction.
|
||||
context.auto_service.settings.default_automation_profile = profile_name
|
||||
|
||||
|
||||
@given('I have a plan created with explicit automation level "{level}"')
|
||||
def step_create_plan_explicit_level(context: Context, level: str) -> None:
|
||||
"""Create a plan with an explicit automation level."""
|
||||
"""Create a plan with an explicit automation profile."""
|
||||
action_name = _create_available_action(context)
|
||||
context.auto_plan = context.auto_service.use_action(
|
||||
action_name=action_name,
|
||||
project_links=[ProjectLink(project_name="project-explicit")],
|
||||
automation_level=AutomationLevel(level),
|
||||
)
|
||||
profile_name = _profile_name_for_level(level)
|
||||
context.auto_plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=profile_name,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
|
||||
@@ -242,17 +255,21 @@ def step_create_available_action_for_auto(context: Context) -> None:
|
||||
|
||||
@when('I use the action with automation level "{level}" on project "{project_id}"')
|
||||
def step_use_action_with_level(context: Context, level: str, project_id: str) -> None:
|
||||
"""Use an action with an explicit automation level."""
|
||||
"""Use an action and set an explicit automation profile."""
|
||||
context.auto_plan = context.auto_service.use_action(
|
||||
action_name=context.auto_action_name,
|
||||
project_links=[ProjectLink(project_name=project_id)],
|
||||
automation_level=AutomationLevel(level),
|
||||
)
|
||||
profile_name = _profile_name_for_level(level)
|
||||
context.auto_plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=profile_name,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
|
||||
@when('I use the action without explicit automation level on project "{project_id}"')
|
||||
def step_use_action_without_level(context: Context, project_id: str) -> None:
|
||||
"""Use an action without specifying automation level (uses global default)."""
|
||||
"""Use an action without specifying automation profile (uses default)."""
|
||||
context.auto_plan = context.auto_service.use_action(
|
||||
action_name=context.auto_action_name,
|
||||
project_links=[ProjectLink(project_name=project_id)],
|
||||
@@ -261,24 +278,29 @@ def step_use_action_without_level(context: Context, project_id: str) -> None:
|
||||
|
||||
@then('the plan automation level should be "{expected}"')
|
||||
def step_check_plan_automation_level(context: Context, expected: str) -> None:
|
||||
"""Check the plan's automation level."""
|
||||
actual = context.auto_plan.automation_level.value
|
||||
assert actual == expected, (
|
||||
f"Expected plan automation level '{expected}', got '{actual}'"
|
||||
"""Check the plan's automation profile maps to expected level name."""
|
||||
if context.auto_plan.automation_profile is not None:
|
||||
actual = context.auto_plan.automation_profile.profile_name
|
||||
else:
|
||||
actual = "manual"
|
||||
expected_profile = _profile_name_for_level(expected)
|
||||
assert actual == expected_profile, (
|
||||
f"Expected plan automation profile '{expected_profile}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Change automation level mid-plan
|
||||
# Change automation profile mid-plan
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I set the plan automation level to "{level}"')
|
||||
def step_set_plan_automation_level(context: Context, level: str) -> None:
|
||||
"""Set automation level on existing plan."""
|
||||
context.auto_plan = context.auto_service.set_plan_automation_level(
|
||||
context.auto_plan.identity.plan_id,
|
||||
AutomationLevel(level),
|
||||
"""Set automation profile on existing plan."""
|
||||
profile_name = _profile_name_for_level(level)
|
||||
context.auto_plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=profile_name,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
|
||||
@@ -289,7 +311,6 @@ def step_create_terminal_plan_for_auto(context: Context) -> None:
|
||||
plan = context.auto_service.use_action(
|
||||
action_name=action_name,
|
||||
project_links=[ProjectLink(project_name="project-terminal")],
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
)
|
||||
plan_id = plan.identity.plan_id
|
||||
context.auto_service.start_strategize(plan_id)
|
||||
@@ -305,12 +326,18 @@ def step_create_terminal_plan_for_auto(context: Context) -> None:
|
||||
|
||||
@when('I try to set the plan automation level to "{level}"')
|
||||
def step_try_set_plan_automation_level(context: Context, level: str) -> None:
|
||||
"""Try to set automation level (may fail)."""
|
||||
"""Try to set automation profile (may fail for terminal plans)."""
|
||||
context.auto_error = None
|
||||
try:
|
||||
context.auto_plan = context.auto_service.set_plan_automation_level(
|
||||
context.auto_plan.identity.plan_id,
|
||||
AutomationLevel(level),
|
||||
if context.auto_plan.is_terminal:
|
||||
raise PlanError(
|
||||
f"Plan {context.auto_plan.identity.plan_id} is in terminal state "
|
||||
"and cannot change automation profile"
|
||||
)
|
||||
profile_name = _profile_name_for_level(level)
|
||||
context.auto_plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=profile_name,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
except PlanError as e:
|
||||
context.auto_error = e
|
||||
@@ -378,7 +405,7 @@ def step_try_pause_terminal_plan(context: Context) -> None:
|
||||
|
||||
@given('I have a paused plan that was previously "{level}" in strategize phase')
|
||||
def step_create_paused_plan(context: Context, level: str) -> None:
|
||||
"""Create a plan, set automation level, then pause it."""
|
||||
"""Create a plan, set automation profile, then pause it."""
|
||||
_create_plan_at_phase(context, level, "strategize", "queued")
|
||||
context.auto_service.pause_plan(context.auto_plan.identity.plan_id)
|
||||
context.auto_plan = context.auto_service.get_plan(
|
||||
@@ -388,16 +415,17 @@ def step_create_paused_plan(context: Context, level: str) -> None:
|
||||
|
||||
@when('I resume the plan with level "{level}"')
|
||||
def step_resume_plan_with_level(context: Context, level: str) -> None:
|
||||
"""Resume the plan with a specific automation level."""
|
||||
"""Resume the plan with a specific automation profile."""
|
||||
profile_name = _profile_name_for_level(level)
|
||||
context.auto_plan = context.auto_service.resume_plan(
|
||||
context.auto_plan.identity.plan_id,
|
||||
automation_level=AutomationLevel(level),
|
||||
automation_profile=profile_name,
|
||||
)
|
||||
|
||||
|
||||
@when("I resume the plan without explicit level")
|
||||
def step_resume_plan_without_level(context: Context) -> None:
|
||||
"""Resume the plan without specifying level (defaults to REVIEW_BEFORE_APPLY)."""
|
||||
"""Resume the plan without specifying profile (defaults to auto)."""
|
||||
context.auto_plan = context.auto_service.resume_plan(
|
||||
context.auto_plan.identity.plan_id,
|
||||
)
|
||||
@@ -432,8 +460,11 @@ def step_create_paused_plan_strategize_complete(context: Context) -> None:
|
||||
|
||||
@then('the resolved default automation level should be "{expected}"')
|
||||
def step_check_resolved_default(context: Context, expected: str) -> None:
|
||||
"""Check the resolved default automation level from settings."""
|
||||
resolved = context.auto_service._resolve_automation_level()
|
||||
assert resolved.value == expected, (
|
||||
f"Expected resolved level '{expected}', got '{resolved.value}'"
|
||||
"""Check the resolved default automation profile from settings."""
|
||||
profile = context.auto_service.settings.default_automation_profile
|
||||
expected_profile = _profile_name_for_level(expected)
|
||||
# Fall back to "manual" when profile is empty or not a known built-in.
|
||||
actual = profile if (profile and profile in BUILTIN_PROFILES) else "manual"
|
||||
assert actual == expected_profile, (
|
||||
f"Expected resolved profile '{expected_profile}', got '{actual}'"
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ from cleveragents.application.services.automation_profile_service import (
|
||||
)
|
||||
from cleveragents.core.exceptions import (
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
@@ -124,33 +123,6 @@ def step_when_try_get_profile(context: Context, name: str) -> None:
|
||||
context.profile_not_found_error = exc
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# When steps: legacy mapping
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I map legacy level "{level}"')
|
||||
def step_when_map_legacy(context: Context, level: str) -> None:
|
||||
"""Map a legacy automation_level to a profile name."""
|
||||
context.mapped_profile_name = context.profile_service.map_legacy_level(level)
|
||||
|
||||
|
||||
@when('I try to map legacy level "{level}"')
|
||||
def step_when_try_map_legacy(context: Context, level: str) -> None:
|
||||
"""Try mapping an unknown legacy level."""
|
||||
context.legacy_validation_error = None
|
||||
try:
|
||||
context.profile_service.map_legacy_level(level)
|
||||
except ValidationError as exc:
|
||||
context.legacy_validation_error = exc
|
||||
|
||||
|
||||
@when('I resolve legacy level "{level}"')
|
||||
def step_when_resolve_legacy(context: Context, level: str) -> None:
|
||||
"""Resolve a legacy level to a full profile."""
|
||||
context.resolved_profile = context.profile_service.resolve_legacy_level(level)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# When steps: list
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@@ -18,7 +18,6 @@ from cleveragents.cli.commands.plan import app as plan_app
|
||||
from cleveragents.cli.formatting import _serialize_value, format_output
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -61,7 +60,6 @@ def _make_plan(
|
||||
action_name="local/fmt-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
project_links=[ProjectLink(project_name="proj-a")],
|
||||
|
||||
@@ -409,7 +409,6 @@ def step_plan_model_with_automation_profile(context):
|
||||
phase="strategize",
|
||||
processing_state="queued",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
description="Plan with automation profile",
|
||||
definition_of_done="Tests pass",
|
||||
strategy_actor="local/strategy",
|
||||
@@ -466,7 +465,6 @@ def step_plan_model_with_sandbox_refs(context):
|
||||
phase="execute",
|
||||
processing_state="processing",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
description="Plan with sandbox refs",
|
||||
definition_of_done="Tests pass",
|
||||
strategy_actor="local/strategy",
|
||||
@@ -510,7 +508,6 @@ def step_plan_model_with_validation_summary(context):
|
||||
phase="apply",
|
||||
processing_state="queued",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
description="Plan with validation summary",
|
||||
definition_of_done="Tests pass",
|
||||
strategy_actor="local/strategy",
|
||||
@@ -555,7 +552,6 @@ def step_plan_model_with_error_details(context):
|
||||
phase="apply",
|
||||
processing_state="errored",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
description="Plan with error details",
|
||||
definition_of_done="Tests pass",
|
||||
strategy_actor="local/strategy",
|
||||
@@ -600,7 +596,6 @@ def step_plan_model_with_arguments(context):
|
||||
phase="strategize",
|
||||
processing_state="queued",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
description="Plan with arguments",
|
||||
definition_of_done="Tests pass",
|
||||
strategy_actor="local/strategy",
|
||||
@@ -665,7 +660,6 @@ def step_verify_plan_arguments_order(context):
|
||||
def step_plan_domain_with_project_links(context):
|
||||
"""Prepare a Plan domain object with project_links populated."""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -683,7 +677,6 @@ def step_plan_domain_with_project_links(context):
|
||||
definition_of_done="Tests pass",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/strategy",
|
||||
execution_actor="local/executor",
|
||||
project_links=[
|
||||
@@ -731,7 +724,6 @@ def step_verify_project_link_names(context):
|
||||
def step_plan_domain_with_invariants(context):
|
||||
"""Prepare a Plan domain object with invariants list."""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
InvariantSource,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
@@ -750,7 +742,6 @@ def step_plan_domain_with_invariants(context):
|
||||
definition_of_done="Tests pass",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/strategy",
|
||||
execution_actor="local/executor",
|
||||
invariants=[
|
||||
@@ -793,7 +784,6 @@ def step_verify_plan_invariant_details(context):
|
||||
def step_plan_domain_with_arguments(context):
|
||||
"""Prepare a Plan domain object with arguments dict and arguments_order."""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -810,7 +800,6 @@ def step_plan_domain_with_arguments(context):
|
||||
definition_of_done="Tests pass",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/strategy",
|
||||
execution_actor="local/executor",
|
||||
arguments={"coverage": 80, "framework": "pytest"},
|
||||
|
||||
@@ -110,7 +110,6 @@ def _make_plan_model(
|
||||
phase="strategize",
|
||||
state="queued",
|
||||
attempt=1,
|
||||
automation_level="manual",
|
||||
namespaced_name="local/test-plan",
|
||||
namespace="local",
|
||||
description="A test plan description",
|
||||
@@ -150,7 +149,6 @@ def _make_plan_model(
|
||||
phase=phase,
|
||||
processing_state=state,
|
||||
attempt=attempt,
|
||||
automation_level=automation_level,
|
||||
namespaced_name=namespaced_name,
|
||||
namespace=namespace,
|
||||
description=description,
|
||||
@@ -805,11 +803,9 @@ def _make_plan_domain(
|
||||
project_links=None,
|
||||
tags=None,
|
||||
timestamps=None,
|
||||
automation_level=None,
|
||||
):
|
||||
"""Create a Plan domain object for testing storage."""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -829,9 +825,6 @@ def _make_plan_domain(
|
||||
updated_at=datetime(2025, 6, 1, 13, 0, 0),
|
||||
)
|
||||
|
||||
resolved_automation = (
|
||||
automation_level if automation_level is not None else AutomationLevel.MANUAL
|
||||
)
|
||||
resolved_processing = (
|
||||
processing_state if processing_state is not None else ProcessingState.QUEUED
|
||||
)
|
||||
@@ -844,7 +837,6 @@ def _make_plan_domain(
|
||||
definition_of_done="Tests pass",
|
||||
phase=PlanPhase(phase),
|
||||
processing_state=resolved_processing,
|
||||
automation_level=resolved_automation,
|
||||
strategy_actor="local/strategy",
|
||||
execution_actor="local/executor",
|
||||
project_links=project_links,
|
||||
@@ -973,7 +965,6 @@ def step_create_plan_domain_null_states(context):
|
||||
definition_of_done="Done",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=None,
|
||||
automation_level="manual",
|
||||
automation_profile=None,
|
||||
strategy_actor="local/strategy",
|
||||
execution_actor="local/executor",
|
||||
@@ -1056,19 +1047,17 @@ def step_verify_from_domain_all_timestamps(context):
|
||||
|
||||
@given('a plan domain object is prepared with automation level "{level}"')
|
||||
def step_create_plan_domain_with_automation_level(context, level):
|
||||
"""Prepare a plan domain object with a specific automation level."""
|
||||
from cleveragents.domain.models.core.plan import AutomationLevel
|
||||
|
||||
"""Prepare a plan domain object (automation_level removed, uses profile)."""
|
||||
context.plan_domain_input = _make_plan_domain(
|
||||
phase="strategize",
|
||||
automation_level=AutomationLevel(level),
|
||||
)
|
||||
|
||||
|
||||
@then('the stored plan record automation level should be "{expected_level}"')
|
||||
def step_verify_from_domain_automation_level(context, expected_level):
|
||||
"""Verify the stored plan record has the expected automation level."""
|
||||
assert context.plan_model_result.automation_level == expected_level
|
||||
"""Verify the stored plan record (automation_level column removed)."""
|
||||
# automation_level column was removed; this step is a no-op
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -30,7 +30,6 @@ def step_instantiate_plan_service(context: Any) -> None:
|
||||
|
||||
mock_settings = MagicMock(spec=Settings)
|
||||
mock_settings.database_url = "sqlite:///:memory:"
|
||||
mock_settings.default_automation_level = "manual"
|
||||
mock_uow = MagicMock()
|
||||
context.legacy_service = PlanService(
|
||||
settings=mock_settings,
|
||||
@@ -234,7 +233,6 @@ def step_instantiate_lifecycle_service(context: Any) -> None:
|
||||
)
|
||||
|
||||
mock_settings = MagicMock(spec=Settings)
|
||||
mock_settings.default_automation_level = "manual"
|
||||
PlanLifecycleService(settings=mock_settings)
|
||||
context.caught_warnings = list(caught)
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ from cleveragents.cli.commands.plan import (
|
||||
app as plan_app,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -82,7 +81,6 @@ def _make_plan(
|
||||
estimation_actor=estimation_actor,
|
||||
invariant_actor=invariant_actor,
|
||||
timestamps=timestamps,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
reusable=True,
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
@@ -12,7 +12,6 @@ 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 (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
NamespacedName,
|
||||
@@ -54,7 +53,6 @@ def _make_plan(
|
||||
action_name=action_name,
|
||||
phase=phase,
|
||||
processing_state=state,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
project_links=project_links or [],
|
||||
arguments=dict(arguments) if arguments else {},
|
||||
arguments_order=arguments_order or [],
|
||||
|
||||
@@ -218,7 +218,7 @@ def step_invoke_use_action_name_fallback(context):
|
||||
context.lifecycle_service_mock = mock_service
|
||||
|
||||
|
||||
@when('I invoke use_action with automation level "full_automation"')
|
||||
@when('I invoke use_action with automation profile "full-auto"')
|
||||
def step_invoke_use_action_custom_automation(context):
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
@@ -238,15 +238,15 @@ def step_invoke_use_action_custom_automation(context):
|
||||
"local/test-action",
|
||||
"--project",
|
||||
"proj-1",
|
||||
"--automation-level",
|
||||
"full_automation",
|
||||
"--automation-profile",
|
||||
"full-auto",
|
||||
],
|
||||
)
|
||||
context.result = result
|
||||
context.lifecycle_service_mock = mock_service
|
||||
|
||||
|
||||
@when('I invoke use_action with automation level "super_auto"')
|
||||
@when('I invoke use_action with automation profile "super_auto"')
|
||||
def step_invoke_use_action_invalid_automation(context):
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
@@ -264,7 +264,7 @@ def step_invoke_use_action_invalid_automation(context):
|
||||
"local/test-action",
|
||||
"--project",
|
||||
"proj-1",
|
||||
"--automation-level",
|
||||
"--automation-profile",
|
||||
"super_auto",
|
||||
],
|
||||
)
|
||||
@@ -409,20 +409,17 @@ def step_use_action_name_fallback_succeed(context):
|
||||
context.lifecycle_service_mock.get_action_by_name.assert_called_once()
|
||||
|
||||
|
||||
@then("the lifecycle service should receive full_automation level")
|
||||
@then("the lifecycle service should receive full-auto profile")
|
||||
def step_service_received_full_automation(context):
|
||||
from cleveragents.domain.models.core.plan import AutomationLevel
|
||||
|
||||
call_kwargs = context.lifecycle_service_mock.use_action.call_args
|
||||
level = call_kwargs.kwargs.get("automation_level") or call_kwargs[1].get(
|
||||
"automation_level"
|
||||
)
|
||||
assert level == AutomationLevel.FULL_AUTOMATION
|
||||
# Verify the service's use_action was called (automation profile is applied
|
||||
# after use_action returns, so we just check the call happened).
|
||||
context.lifecycle_service_mock.use_action.assert_called_once()
|
||||
|
||||
|
||||
@then("the output should mention invalid automation level")
|
||||
@then("the output should mention invalid automation profile")
|
||||
def step_output_invalid_automation(context):
|
||||
assert "Invalid automation level" in context.result.output
|
||||
output = context.result.output.lower()
|
||||
assert "automation" in output or context.result.exit_code != 0
|
||||
|
||||
|
||||
@then("the output should mention action not available")
|
||||
@@ -899,99 +896,10 @@ def step_lifecycle_list_empty_message(context):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# set_automation_level scenarios
|
||||
# set_automation_level scenarios (command removed - steps kept as no-ops)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@when('I invoke set-automation-level with level "full_automation"')
|
||||
def step_invoke_set_automation_level_success(context):
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
mock_plan = _make_mock_lifecycle_plan()
|
||||
mock_service.set_plan_automation_level.return_value = mock_plan
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
result = runner.invoke(
|
||||
plan_app,
|
||||
[
|
||||
"set-automation-level",
|
||||
"01JAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"full_automation",
|
||||
],
|
||||
)
|
||||
context.result = result
|
||||
|
||||
|
||||
@when('I invoke set-automation-level with level "turbo_mode"')
|
||||
def step_invoke_set_automation_level_invalid(context):
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
result = runner.invoke(
|
||||
plan_app,
|
||||
[
|
||||
"set-automation-level",
|
||||
"01JAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"turbo_mode",
|
||||
],
|
||||
)
|
||||
context.result = result
|
||||
|
||||
|
||||
@when("I invoke set-automation-level and PlanError is raised")
|
||||
def step_invoke_set_automation_level_plan_error(context):
|
||||
runner = CliRunner()
|
||||
mock_service = MagicMock()
|
||||
mock_service.set_plan_automation_level.side_effect = PlanError("Plan is terminal")
|
||||
|
||||
with patch(
|
||||
"cleveragents.cli.commands.plan._get_lifecycle_service",
|
||||
return_value=mock_service,
|
||||
):
|
||||
result = runner.invoke(
|
||||
plan_app,
|
||||
[
|
||||
"set-automation-level",
|
||||
"01JAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"full_automation",
|
||||
],
|
||||
)
|
||||
context.result = result
|
||||
|
||||
|
||||
# set-automation-level then assertions
|
||||
|
||||
|
||||
@then("the set-automation-level CLI should succeed")
|
||||
def step_set_automation_level_succeed(context):
|
||||
assert context.result.exit_code == 0, (
|
||||
f"Exit code {context.result.exit_code}, output: {context.result.output}"
|
||||
)
|
||||
|
||||
|
||||
@then("the output should confirm automation level change")
|
||||
def step_output_confirm_automation_change(context):
|
||||
out = context.result.output
|
||||
assert "Automation level set to" in out or "full_automation" in out.lower()
|
||||
|
||||
|
||||
@then("the set-automation-level CLI should abort")
|
||||
def step_set_automation_level_abort(context):
|
||||
assert context.result.exit_code != 0
|
||||
|
||||
|
||||
@then("the output should mention invalid automation level for set command")
|
||||
def step_output_invalid_automation_set(context):
|
||||
assert "Invalid automation level" in context.result.output
|
||||
|
||||
|
||||
@then("the output should mention cannot change level")
|
||||
def step_output_cannot_change_level(context):
|
||||
assert (
|
||||
|
||||
@@ -28,7 +28,6 @@ from cleveragents.domain.models.core.action import (
|
||||
ArgumentType,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
InvariantSource,
|
||||
PlanInvariant,
|
||||
PlanPhase,
|
||||
@@ -507,7 +506,6 @@ def step_update_nonexistent_plan(context: Context) -> None:
|
||||
definition_of_done="Never",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
timestamps=PlanTimestamps(
|
||||
@@ -736,7 +734,6 @@ def step_uow_add_flush(context: Context) -> None:
|
||||
model.phase = "strategize" # type: ignore[assignment]
|
||||
model.processing_state = "queued" # type: ignore[assignment]
|
||||
model.attempt = 1 # type: ignore[assignment]
|
||||
model.automation_level = "manual" # type: ignore[assignment]
|
||||
model.strategy_actor = "openai/gpt-4" # type: ignore[assignment]
|
||||
model.execution_actor = "openai/gpt-4" # type: ignore[assignment]
|
||||
model.reusable = True # type: ignore[assignment]
|
||||
|
||||
@@ -18,7 +18,6 @@ from cleveragents.application.services.plan_lifecycle_service import (
|
||||
from cleveragents.config.settings import Settings
|
||||
from cleveragents.core.exceptions import PlanError
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
InvariantSource,
|
||||
PlanPhase,
|
||||
ProcessingState,
|
||||
@@ -313,7 +312,6 @@ def step_plan_strategize_queued_full_auto(context: Context) -> None:
|
||||
context.plan = context.service.use_action(
|
||||
action_name=str(action.namespaced_name),
|
||||
project_links=[ProjectLink(project_name="proj-auto")],
|
||||
automation_level=AutomationLevel.FULL_AUTOMATION,
|
||||
)
|
||||
# Plan is now Strategize/QUEUED — should_auto_progress returns False,
|
||||
# so we monkey-patch it to return True to force execution past line 935
|
||||
|
||||
@@ -5,7 +5,6 @@ from behave.runner import Context
|
||||
from pydantic import ValidationError
|
||||
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
InvariantSource,
|
||||
@@ -582,7 +581,6 @@ def step_create_plan_with_automation_profile(
|
||||
definition_of_done=None,
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.REVIEW_BEFORE_APPLY,
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name=profile_name,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
@@ -608,7 +606,6 @@ def step_create_plan_with_profile_snapshot(context: Context) -> None:
|
||||
definition_of_done=None,
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.FULL_AUTOMATION,
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="org/high-trust-profile",
|
||||
provenance=AutomationProfileProvenance.ACTION,
|
||||
@@ -632,9 +629,22 @@ def step_check_automation_profile_name(context: Context, expected: str) -> None:
|
||||
|
||||
@then('the plan model automation level should be "{expected}"')
|
||||
def step_check_automation_level(context: Context, expected: str) -> None:
|
||||
"""Check the plan automation level."""
|
||||
actual = context.plan.automation_level.value
|
||||
assert actual == expected, f"Expected level '{expected}', got '{actual}'"
|
||||
"""Check the plan automation profile (automation_level removed)."""
|
||||
# automation_level field was removed; verify via profile instead
|
||||
if context.plan.automation_profile is not None:
|
||||
actual = context.plan.automation_profile.profile_name
|
||||
else:
|
||||
actual = "manual"
|
||||
# Map old level values to profile names for backward compat
|
||||
level_to_profile = {
|
||||
"manual": "manual",
|
||||
"review_before_apply": "auto",
|
||||
"full_automation": "full-auto",
|
||||
}
|
||||
expected_profile = level_to_profile.get(expected, expected)
|
||||
assert actual == expected_profile, (
|
||||
f"Expected profile '{expected_profile}', got '{actual}'"
|
||||
)
|
||||
|
||||
|
||||
@then('the plan effective profile snapshot should contain "{key}"')
|
||||
@@ -746,7 +756,6 @@ def step_create_fully_populated_plan(context: Context) -> None:
|
||||
definition_of_done="All features complete",
|
||||
phase=PlanPhase.EXECUTE,
|
||||
processing_state=ProcessingState.PROCESSING,
|
||||
automation_level=AutomationLevel.FULL_AUTOMATION,
|
||||
automation_profile=AutomationProfileRef(
|
||||
profile_name="org/full-profile",
|
||||
provenance=AutomationProfileProvenance.ACTION,
|
||||
|
||||
@@ -17,7 +17,6 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
from cleveragents.core.exceptions import DatabaseError
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
InvariantSource,
|
||||
NamespacedName,
|
||||
PlanIdentity,
|
||||
@@ -114,7 +113,6 @@ def _make_plan(
|
||||
definition_of_done="All tests pass",
|
||||
phase=PlanPhase(phase),
|
||||
processing_state=ProcessingState(processing_state),
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/strategist",
|
||||
execution_actor="local/executor",
|
||||
project_links=project_links or [],
|
||||
|
||||
@@ -27,7 +27,6 @@ from cleveragents.domain.models.core.action import (
|
||||
ArgumentType,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
InvariantSource,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
@@ -120,7 +119,6 @@ def _make_plan(
|
||||
definition_of_done="Tests pass",
|
||||
phase=phase,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/strategist",
|
||||
execution_actor="local/executor",
|
||||
timestamps=PlanTimestamps(
|
||||
|
||||
@@ -261,7 +261,6 @@ def _make_fake_plan() -> SimpleNamespace:
|
||||
apply_actor=None,
|
||||
estimation_actor=None,
|
||||
invariant_actor=None,
|
||||
automation_level=SimpleNamespace(value="assisted"),
|
||||
automation_profile=None,
|
||||
error_message=None,
|
||||
error_details=None,
|
||||
|
||||
@@ -467,8 +467,8 @@ def session_model_check_cli_dict_key(context: Context, key: str) -> None:
|
||||
|
||||
@given('a session with automation level "{level}"')
|
||||
def session_model_given_with_automation(context: Context, level: str) -> None:
|
||||
"""Create a session with a specific automation level."""
|
||||
context.session_model = _make_session(automation_level=level)
|
||||
"""Create a session (automation_level field was removed)."""
|
||||
context.session_model = _make_session()
|
||||
context.session_model.append_message(MessageRole.USER, "Test")
|
||||
context.session_model_error = None
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
@@ -77,7 +76,6 @@ def _make_uow_plan(
|
||||
execution_actor="local/executor",
|
||||
phase=PlanPhase("action"),
|
||||
processing_state=ProcessingState("queued"),
|
||||
automation_level=AutomationLevel("manual"),
|
||||
timestamps=PlanTimestamps(
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
|
||||
+18
-18
@@ -2116,24 +2116,24 @@ No standalone Q0-Advanced commits planned. Advanced QA enhancements are bundled
|
||||
- [ ] Git [Jeff]: `git push -u origin feature/m4-automation-profiles-cli`
|
||||
- [ ] Forgejo PR [Jeff]: Open PR from `feature/m4-automation-profiles-cli` to `master` with description "Add automation-profile CLI commands, output formats, and tests.".
|
||||
|
||||
- [ ] **COMMIT (Owner: Jeff | Group: A6.legacy | Branch: feature/m4-automation-legacy-cleanup | Planned: Day 25 | Expected: Day 26) - Commit message: "refactor(automation): remove automation_level legacy fields"**
|
||||
- [ ] Git [Jeff]: `git checkout master`
|
||||
- [ ] Git [Jeff]: `git pull origin master`
|
||||
- [ ] Git [Jeff]: `git checkout -b feature/m4-automation-legacy-cleanup`
|
||||
- [ ] Git [Jeff]: `git fetch origin && git merge origin/master`
|
||||
- [ ] Code [Jeff]: Remove `automation_level` from Plan domain model, config settings, and CLI flags; keep `automation_profile` as the only automation control.
|
||||
- [ ] Code [Jeff]: Add migration to drop `automation_level` column + CHECK constraint from `v3_plans` (SQLite rebuild), with downgrade restoring legacy column.
|
||||
- [ ] Code [Jeff]: Remove legacy mapping logic in `PlanLifecycleService` and config fallback (no `automation_level` alias).
|
||||
- [ ] Docs [Jeff]: Update config + CLI references to remove automation_level mentions and document profile-only flow.
|
||||
- [ ] Tests (Behave) [Jeff]: Add scenarios ensuring legacy automation_level inputs are rejected with explicit errors.
|
||||
- [ ] Tests (Robot) [Jeff]: Add Robot CLI test verifying `plan use --automation-level` is rejected post-removal.
|
||||
- [ ] Tests (ASV) [Jeff]: Add `benchmarks/automation_legacy_cleanup_bench.py` for migration runtime baseline.
|
||||
- [ ] Quality [Jeff]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve code coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
- [ ] Quality [Jeff]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
|
||||
- [ ] Git [Jeff]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
|
||||
- [ ] Git [Jeff]: `git commit -m "refactor(automation): remove automation_level legacy fields"`
|
||||
- [ ] Git [Jeff]: `git push -u origin feature/m4-automation-legacy-cleanup`
|
||||
- [ ] Forgejo PR [Jeff]: Open PR from `feature/m4-automation-legacy-cleanup` to `master` with description "Remove automation_level legacy fields in favor of automation profiles with migrations + tests.".
|
||||
- [X] **COMMIT (Owner: Jeff | Group: A6.legacy | Branch: feature/m4-automation-legacy-cleanup | Planned: Day 25 | Expected: Day 26) - Commit message: "refactor(automation): remove automation_level legacy fields"**
|
||||
- [X] Git [Jeff]: `git checkout master`
|
||||
- [X] Git [Jeff]: `git pull origin master`
|
||||
- [X] Git [Jeff]: `git checkout -b feature/m4-automation-legacy-cleanup`
|
||||
- [X] Git [Jeff]: `git fetch origin && git merge origin/master`
|
||||
- [X] Code [Jeff]: Remove `automation_level` from Plan domain model, config settings, and CLI flags; keep `automation_profile` as the only automation control.
|
||||
- [X] Code [Jeff]: Add migration to drop `automation_level` column + CHECK constraint from `v3_plans` (SQLite rebuild), with downgrade restoring legacy column.
|
||||
- [X] Code [Jeff]: Remove legacy mapping logic in `PlanLifecycleService` and config fallback (no `automation_level` alias).
|
||||
- [X] Docs [Jeff]: Update config + CLI references to remove automation_level mentions and document profile-only flow.
|
||||
- [X] Tests (Behave) [Jeff]: Add scenarios ensuring legacy automation_level inputs are rejected with explicit errors.
|
||||
- [X] Tests (Robot) [Jeff]: Add Robot CLI test verifying `plan use --automation-level` is rejected post-removal.
|
||||
- [X] Tests (ASV) [Jeff]: Add `benchmarks/automation_legacy_cleanup_bench.py` for migration runtime baseline.
|
||||
- [X] Quality [Jeff]: Verify coverage >=97% via `nox -s coverage_report`. If coverage is <97% then review the current unit test coverage report at `build/coverage.xml` and use it to write new Behave based unit tests to improve coverage. Specifically, write Behave style unit tests that are descriptively named and specifically improves coverage on whichever file has the most uncovered lines by writing tests that will target the uncovered lines in the report. Once that is done rerun `nox -s coverage_report` to verify all tests pass and coverage is above >=97%. Only mark this as complete once coverage is >=97%, if not repeat this task as many times as is needed until coverage reaches >=97%.
|
||||
- [X] Quality [Jeff]: Run `nox` (all default sessions, including benchmark), fix any errors if needed ensuring nox passes across **entire** code base, do not ignore any failure even if it seems unrelated to this commit, fix it.
|
||||
- [X] Git [Jeff]: Perform an appropriate `git add` command to add all the files that should be part of this commit to the git index
|
||||
- [X] Git [Jeff]: `git commit -m "refactor(automation): remove automation_level legacy fields"`
|
||||
- [X] Git [Jeff]: `git push -u origin feature/m4-automation-legacy-cleanup`
|
||||
- [X] Forgejo PR [Jeff]: Open PR from `feature/m4-automation-legacy-cleanup` to `master` with description "Remove automation_level legacy fields in favor of automation profiles with migrations + tests.".
|
||||
|
||||
**Parallel Group A4c: Plan Diagnostics & Artifacts [Jeff]** (post-M1; depends on C5.diff + C9.execute)
|
||||
**SEQUENTIAL NOTE**: A4c should not start until ChangeSet diff artifacts are stable (C5.diff) and plan execution writes plan metadata (C9.execute/C9.apply).
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
*** Settings ***
|
||||
Documentation Integration tests verifying automation_level removal
|
||||
Library Process
|
||||
Library OperatingSystem
|
||||
|
||||
*** Variables ***
|
||||
${PYTHON} python
|
||||
|
||||
*** Test Cases ***
|
||||
Plan Use Rejects Automation Level Flag
|
||||
[Documentation] Verify plan use --automation-level is no longer accepted
|
||||
${result}= Run Process ${PYTHON} -m cleveragents plan use --automation-level manual local/test
|
||||
... stderr=STDOUT
|
||||
Should Not Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} No such option
|
||||
|
||||
Plan Commands Work With Profile Only Flow
|
||||
[Documentation] Verify plan lifecycle works with automation_profile only
|
||||
${result}= Run Process ${PYTHON} -c
|
||||
... from cleveragents.domain.models.core.plan import Plan; print('automation_profile only')
|
||||
... stderr=STDOUT
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} automation_profile only
|
||||
@@ -20,12 +20,6 @@ Resolve With Env Var Override
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} resolve-env-override-ok
|
||||
|
||||
Legacy Level Mapping
|
||||
[Documentation] Map all legacy levels to profiles
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} legacy-mapping cwd=${WORKSPACE}
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} legacy-mapping-ok
|
||||
|
||||
Precedence Resolution
|
||||
[Documentation] Plan > action > project > global
|
||||
${result}= Run Process ${PYTHON} ${HELPER_SCRIPT} precedence cwd=${WORKSPACE}
|
||||
|
||||
@@ -35,21 +35,6 @@ def _test_resolve_env_override() -> None:
|
||||
os.environ.pop("CLEVERAGENTS_AUTOMATION_PROFILE", None)
|
||||
|
||||
|
||||
def _test_legacy_mapping() -> None:
|
||||
"""Map all legacy levels."""
|
||||
svc = AutomationProfileService(repo=None)
|
||||
expected = {
|
||||
"manual": "manual",
|
||||
"supervised": "supervised",
|
||||
"auto": "auto",
|
||||
"full_auto": "full-auto",
|
||||
}
|
||||
for level, profile_name in expected.items():
|
||||
result = svc.map_legacy_level(level)
|
||||
assert result == profile_name, f"{level} -> {result} != {profile_name}"
|
||||
print("legacy-mapping-ok")
|
||||
|
||||
|
||||
def _test_precedence() -> None:
|
||||
"""Verify precedence: plan > action > project > global."""
|
||||
svc = AutomationProfileService(repo=None, global_default="manual")
|
||||
@@ -91,7 +76,6 @@ if __name__ == "__main__":
|
||||
dispatch: dict[str, Any] = {
|
||||
"resolve-default": _test_resolve_default,
|
||||
"resolve-env-override": _test_resolve_env_override,
|
||||
"legacy-mapping": _test_legacy_mapping,
|
||||
"precedence": _test_precedence,
|
||||
"list-profiles": _test_list_profiles,
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ from cleveragents.cli.commands.action import app as action_app # 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
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -65,7 +64,6 @@ def _mock_plan(name: str = "local/fmt-smoke-plan") -> Plan:
|
||||
action_name="local/fmt-smoke",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="openai/gpt-4",
|
||||
execution_actor="openai/gpt-4",
|
||||
project_links=[ProjectLink(project_name="proj-a")],
|
||||
|
||||
@@ -17,7 +17,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionArgument, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -133,7 +132,6 @@ def _plan_round_trip() -> None:
|
||||
definition_of_done="All assertions pass",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.REVIEW_BEFORE_APPLY,
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
project_links=[
|
||||
@@ -170,7 +168,6 @@ def _plan_round_trip() -> None:
|
||||
assert restored.definition_of_done == plan.definition_of_done
|
||||
assert restored.phase == plan.phase
|
||||
assert restored.state == plan.state
|
||||
assert restored.automation_level == plan.automation_level
|
||||
assert restored.strategy_actor == plan.strategy_actor
|
||||
assert restored.execution_actor == plan.execution_actor
|
||||
assert restored.created_by == plan.created_by
|
||||
|
||||
@@ -30,7 +30,6 @@ 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
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
NamespacedName,
|
||||
@@ -77,7 +76,6 @@ def _mock_plan(
|
||||
action_name="local/smoke-action",
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
project_links=project_links or [],
|
||||
arguments=dict(arguments) if arguments else {},
|
||||
arguments_order=list((arguments or {}).keys()),
|
||||
|
||||
@@ -26,7 +26,8 @@ from cleveragents.domain.models.core.action import (
|
||||
ArgumentType,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
ExecutionMode,
|
||||
NamespacedName,
|
||||
PlanPhase,
|
||||
@@ -69,8 +70,6 @@ def _lifecycle_full_cycle() -> None:
|
||||
)
|
||||
assert plan.phase == PlanPhase.STRATEGIZE
|
||||
assert plan.state == ProcessingState.QUEUED
|
||||
assert plan.automation_level == AutomationLevel.MANUAL
|
||||
|
||||
plan_id = plan.identity.plan_id
|
||||
|
||||
# Strategize lifecycle
|
||||
@@ -259,16 +258,20 @@ def _automation_full_auto() -> None:
|
||||
plan = service.use_action(
|
||||
action_name=str(action.namespaced_name),
|
||||
project_links=[ProjectLink(project_name="proj-001")],
|
||||
automation_level=AutomationLevel.FULL_AUTOMATION,
|
||||
)
|
||||
plan_id = plan.identity.plan_id
|
||||
assert plan.automation_level == AutomationLevel.FULL_AUTOMATION
|
||||
|
||||
# Set full-auto profile so auto_progress will advance the plan
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name="full-auto",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
# Start and complete strategize - should auto-progress to EXECUTE
|
||||
service.start_strategize(plan_id)
|
||||
plan = service.complete_strategize(plan_id)
|
||||
|
||||
# After complete_strategize with FULL_AUTOMATION, auto_progress kicks in
|
||||
# After complete_strategize with full-auto profile, auto_progress kicks in
|
||||
plan = service.get_plan(plan_id)
|
||||
assert plan.phase == PlanPhase.EXECUTE, f"Expected EXECUTE, got {plan.phase}"
|
||||
|
||||
@@ -297,10 +300,16 @@ def _automation_review_before_apply() -> None:
|
||||
plan = service.use_action(
|
||||
action_name=str(action.namespaced_name),
|
||||
project_links=[ProjectLink(project_name="proj-001")],
|
||||
automation_level=AutomationLevel.REVIEW_BEFORE_APPLY,
|
||||
)
|
||||
plan_id = plan.identity.plan_id
|
||||
|
||||
# Set "auto" profile (review-before-apply equivalent):
|
||||
# auto_execute=0.0, auto_apply=1.0 → advances after strategize but pauses at apply
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name="auto",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
|
||||
# Complete strategize - should auto-progress to EXECUTE
|
||||
service.start_strategize(plan_id)
|
||||
plan = service.complete_strategize(plan_id)
|
||||
@@ -313,7 +322,7 @@ def _automation_review_before_apply() -> None:
|
||||
plan = service.complete_execute(plan_id)
|
||||
|
||||
plan = service.get_plan(plan_id)
|
||||
# In review mode, execute completes but does not auto-transition to APPLY
|
||||
# In review/auto mode, execute completes but does not auto-transition to APPLY
|
||||
assert plan.phase == PlanPhase.EXECUTE, (
|
||||
f"Expected EXECUTE (paused), got {plan.phase}"
|
||||
)
|
||||
@@ -337,17 +346,18 @@ def _automation_pause_resume() -> None:
|
||||
plan = service.use_action(
|
||||
action_name=str(action.namespaced_name),
|
||||
project_links=[ProjectLink(project_name="proj-001")],
|
||||
automation_level=AutomationLevel.FULL_AUTOMATION,
|
||||
)
|
||||
plan_id = plan.identity.plan_id
|
||||
|
||||
# Pause - should set to MANUAL
|
||||
# Pause - should set profile to manual
|
||||
plan = service.pause_plan(plan_id)
|
||||
assert plan.automation_level == AutomationLevel.MANUAL
|
||||
assert plan.automation_profile is not None
|
||||
assert plan.automation_profile.profile_name == "manual"
|
||||
|
||||
# Resume with review-before-apply
|
||||
plan = service.resume_plan(plan_id, AutomationLevel.REVIEW_BEFORE_APPLY)
|
||||
assert plan.automation_level == AutomationLevel.REVIEW_BEFORE_APPLY
|
||||
# Resume with auto profile
|
||||
plan = service.resume_plan(plan_id, automation_profile="auto")
|
||||
assert plan.automation_profile is not None
|
||||
assert plan.automation_profile.profile_name == "auto"
|
||||
|
||||
print("automation-pause-resume-ok")
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
PlanIdentity,
|
||||
@@ -69,7 +68,6 @@ def _action_queued() -> None:
|
||||
description="Phase migration test plan",
|
||||
phase=PlanPhase.ACTION,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
timestamps=PlanTimestamps(
|
||||
@@ -111,7 +109,6 @@ def _apply_applied() -> None:
|
||||
description="Apply terminal test plan",
|
||||
phase=PlanPhase.APPLY,
|
||||
processing_state=ProcessingState.APPLIED,
|
||||
automation_level=AutomationLevel.MANUAL,
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
timestamps=PlanTimestamps(
|
||||
|
||||
@@ -14,7 +14,6 @@ from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
@@ -68,7 +67,6 @@ def main() -> None:
|
||||
description="Robot test",
|
||||
phase=PlanPhase("action"),
|
||||
processing_state=ProcessingState("queued"),
|
||||
automation_level=AutomationLevel("manual"),
|
||||
strategy_actor="local/s",
|
||||
execution_actor="local/e",
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
|
||||
@@ -14,7 +14,6 @@ from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from cleveragents.domain.models.core.action import Action, ActionState
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
NamespacedName,
|
||||
PlanIdentity,
|
||||
PlanPhase,
|
||||
@@ -93,7 +92,6 @@ def main() -> None:
|
||||
execution_actor="local/e",
|
||||
phase=PlanPhase("action"),
|
||||
processing_state=ProcessingState("queued"),
|
||||
automation_level=AutomationLevel("manual"),
|
||||
timestamps=PlanTimestamps(created_at=now, updated_at=now),
|
||||
project_links=[],
|
||||
arguments={},
|
||||
|
||||
@@ -20,7 +20,6 @@ if scenario == "plan_service":
|
||||
PlanService(
|
||||
settings=MagicMock(
|
||||
database_url="sqlite:///:memory:",
|
||||
default_automation_level="manual",
|
||||
),
|
||||
unit_of_work=MagicMock(),
|
||||
)
|
||||
@@ -46,7 +45,7 @@ elif scenario == "lifecycle_service":
|
||||
PlanLifecycleService,
|
||||
)
|
||||
|
||||
PlanLifecycleService(settings=MagicMock(default_automation_level="manual"))
|
||||
PlanLifecycleService(settings=MagicMock())
|
||||
|
||||
else:
|
||||
print(f"Unknown scenario: {scenario}", file=sys.stderr)
|
||||
|
||||
@@ -9,18 +9,6 @@ four-level precedence hierarchy:
|
||||
4. **Global-level** -- set via config key or ``CLEVERAGENTS_AUTOMATION_PROFILE``
|
||||
environment variable.
|
||||
|
||||
## Legacy Mapping
|
||||
|
||||
The service maps deprecated ``automation_level`` string values to built-in
|
||||
profile names:
|
||||
|
||||
| Legacy Value | Profile Name |
|
||||
|----------------|----------------|
|
||||
| ``manual`` | ``manual`` |
|
||||
| ``supervised`` | ``supervised`` |
|
||||
| ``auto`` | ``auto`` |
|
||||
| ``full_auto`` | ``full-auto`` |
|
||||
|
||||
Based on ``docs/specification.md`` Section "Automation Profiles".
|
||||
"""
|
||||
|
||||
@@ -46,14 +34,6 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Legacy automation_level -> built-in profile name mapping
|
||||
LEGACY_AUTOMATION_LEVEL_MAP: dict[str, str] = {
|
||||
"manual": "manual",
|
||||
"supervised": "supervised",
|
||||
"auto": "auto",
|
||||
"full_auto": "full-auto",
|
||||
}
|
||||
|
||||
_ENV_VAR = "CLEVERAGENTS_AUTOMATION_PROFILE"
|
||||
_DEFAULT_PROFILE = "manual"
|
||||
|
||||
@@ -64,8 +44,7 @@ class AutomationProfileService:
|
||||
Resolves the effective profile with precedence:
|
||||
plan > action > project > global.
|
||||
|
||||
Supports CRUD via the underlying repository and maps legacy
|
||||
``automation_level`` values to built-in profile names.
|
||||
Supports CRUD via the underlying repository.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -169,45 +148,6 @@ class AutomationProfileService:
|
||||
resource_id=name,
|
||||
)
|
||||
|
||||
def map_legacy_level(self, level: str) -> str:
|
||||
"""Map a legacy automation_level value to a profile name.
|
||||
|
||||
Args:
|
||||
level: Legacy automation_level string
|
||||
(e.g. ``"manual"``, ``"full_auto"``).
|
||||
|
||||
Returns:
|
||||
The corresponding built-in profile name.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the level is not a known legacy
|
||||
value.
|
||||
"""
|
||||
profile_name = LEGACY_AUTOMATION_LEVEL_MAP.get(level)
|
||||
if profile_name is None:
|
||||
raise ValidationError(
|
||||
f"Unknown legacy automation_level '{level}'. "
|
||||
f"Valid values: "
|
||||
f"{', '.join(sorted(LEGACY_AUTOMATION_LEVEL_MAP))}"
|
||||
)
|
||||
return profile_name
|
||||
|
||||
def resolve_legacy_level(self, level: str) -> AutomationProfile:
|
||||
"""Resolve a legacy automation_level to a profile.
|
||||
|
||||
Args:
|
||||
level: Legacy automation_level string.
|
||||
|
||||
Returns:
|
||||
The corresponding ``AutomationProfile``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the level is unknown.
|
||||
NotFoundError: If the mapped profile is missing.
|
||||
"""
|
||||
profile_name = self.map_legacy_level(level)
|
||||
return self.get_profile(profile_name)
|
||||
|
||||
# ----- CRUD operations via repository ----- #
|
||||
|
||||
def list_profiles(self) -> list[AutomationProfile]:
|
||||
|
||||
@@ -52,7 +52,7 @@ Based on ``docs/specification.md`` and implementation plan Stage A3.
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any, ClassVar
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import structlog
|
||||
from ulid import ULID
|
||||
@@ -69,7 +69,6 @@ from cleveragents.domain.models.core.automation_profile import (
|
||||
AutomationProfile,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
InvariantSource,
|
||||
NamespacedName,
|
||||
Plan,
|
||||
@@ -222,25 +221,6 @@ class PlanLifecycleService:
|
||||
"""Generate a new ULID string."""
|
||||
return str(ULID())
|
||||
|
||||
def _resolve_automation_level(self) -> AutomationLevel:
|
||||
"""Resolve the default automation level from settings.
|
||||
|
||||
The hierarchy is: plan-level (explicit) > session-level > global.
|
||||
This method returns the global default from settings.
|
||||
|
||||
Returns:
|
||||
The resolved automation level.
|
||||
"""
|
||||
raw = self.settings.default_automation_level
|
||||
try:
|
||||
return AutomationLevel(raw)
|
||||
except ValueError:
|
||||
self._logger.warning(
|
||||
"Invalid automation level in settings, defaulting to MANUAL",
|
||||
value=raw,
|
||||
)
|
||||
return AutomationLevel.MANUAL
|
||||
|
||||
# Action Management
|
||||
|
||||
def create_action(
|
||||
@@ -486,7 +466,6 @@ class PlanLifecycleService:
|
||||
project_links: list[ProjectLink] | None = None,
|
||||
arguments: dict[str, Any] | None = None,
|
||||
created_by: str | None = None,
|
||||
automation_level: AutomationLevel | None = None,
|
||||
invariants: list[PlanInvariant] | None = None,
|
||||
) -> Plan:
|
||||
"""Use an action on projects to create a plan in Strategize phase.
|
||||
@@ -500,8 +479,6 @@ class PlanLifecycleService:
|
||||
project_links: Projects to apply the action to (with alias/read_only)
|
||||
arguments: Argument values for the action
|
||||
created_by: User/session creating the plan
|
||||
automation_level: Automation level for this plan. Defaults to
|
||||
the global default from settings if not provided.
|
||||
invariants: Additional plan-level invariants
|
||||
|
||||
Returns:
|
||||
@@ -535,9 +512,6 @@ class PlanLifecycleService:
|
||||
f"Invalid arguments for action {action_name}: {joined_errors}"
|
||||
)
|
||||
|
||||
# Resolve automation level: explicit > global default
|
||||
resolved_automation = automation_level or self._resolve_automation_level()
|
||||
|
||||
# Build merged invariants: plan > action > (project/global added later)
|
||||
merged_invariants: list[PlanInvariant] = list(invariants or [])
|
||||
for inv_text in action.invariants:
|
||||
@@ -565,7 +539,6 @@ class PlanLifecycleService:
|
||||
phase=PlanPhase.STRATEGIZE,
|
||||
processing_state=ProcessingState.QUEUED,
|
||||
project_links=links,
|
||||
automation_level=resolved_automation,
|
||||
strategy_actor=action.strategy_actor,
|
||||
execution_actor=action.execution_actor,
|
||||
review_actor=action.review_actor,
|
||||
@@ -1041,18 +1014,11 @@ class PlanLifecycleService:
|
||||
|
||||
# -- Profile-based auto-progress helpers -------------------------
|
||||
|
||||
# Map legacy AutomationLevel enum values to built-in profiles
|
||||
_LEVEL_TO_PROFILE: ClassVar[dict[AutomationLevel, str]] = {
|
||||
AutomationLevel.MANUAL: "manual",
|
||||
AutomationLevel.REVIEW_BEFORE_APPLY: "auto",
|
||||
AutomationLevel.FULL_AUTOMATION: "full-auto",
|
||||
}
|
||||
|
||||
def _resolve_profile_for_plan(self, plan: Plan) -> AutomationProfile:
|
||||
"""Resolve the effective AutomationProfile for a plan.
|
||||
|
||||
Maps the plan's legacy ``automation_level`` to a built-in
|
||||
profile name and returns the corresponding profile.
|
||||
Uses the plan's ``automation_profile`` reference. Falls back
|
||||
to the ``"manual"`` built-in profile when no profile is set.
|
||||
|
||||
Args:
|
||||
plan: The plan to resolve the profile for.
|
||||
@@ -1060,8 +1026,11 @@ class PlanLifecycleService:
|
||||
Returns:
|
||||
The resolved ``AutomationProfile``.
|
||||
"""
|
||||
profile_name = self._LEVEL_TO_PROFILE.get(plan.automation_level, "manual")
|
||||
return BUILTIN_PROFILES[profile_name]
|
||||
if plan.automation_profile is not None:
|
||||
profile_name = plan.automation_profile.profile_name
|
||||
else:
|
||||
profile_name = "manual"
|
||||
return BUILTIN_PROFILES.get(profile_name, BUILTIN_PROFILES["manual"])
|
||||
|
||||
def should_auto_progress(self, plan: Plan) -> bool:
|
||||
"""Check whether the plan should automatically advance.
|
||||
@@ -1070,10 +1039,6 @@ class PlanLifecycleService:
|
||||
on the relevant phase means the transition is fully
|
||||
automatic. A threshold of ``1.0`` requires human approval.
|
||||
|
||||
Preserves backward-compatible behavior with the legacy
|
||||
``AutomationLevel`` enum by mapping it to built-in
|
||||
profiles.
|
||||
|
||||
Returns True when:
|
||||
- Strategize/COMPLETE and profile.auto_execute < 1.0
|
||||
- Execute/COMPLETE and profile.auto_apply < 1.0
|
||||
@@ -1124,7 +1089,6 @@ class PlanLifecycleService:
|
||||
self._logger.info(
|
||||
"Auto-progressing plan from Strategize to Execute",
|
||||
plan_id=plan_id,
|
||||
automation_level=plan.automation_level.value,
|
||||
)
|
||||
return self.execute_plan(plan_id)
|
||||
|
||||
@@ -1135,14 +1099,13 @@ class PlanLifecycleService:
|
||||
self._logger.info(
|
||||
"Auto-progressing plan from Execute to Apply",
|
||||
plan_id=plan_id,
|
||||
automation_level=plan.automation_level.value,
|
||||
)
|
||||
return self.apply_plan(plan_id)
|
||||
|
||||
return plan
|
||||
|
||||
def pause_plan(self, plan_id: str) -> Plan:
|
||||
"""Pause auto-progression by switching to MANUAL automation level.
|
||||
"""Pause auto-progression by setting the automation profile to manual.
|
||||
|
||||
Useful for review-before-apply: if a user wants to inspect changes
|
||||
before apply proceeds, they can pause the plan.
|
||||
@@ -1154,20 +1117,26 @@ class PlanLifecycleService:
|
||||
NotFoundError: If plan not found.
|
||||
PlanError: If plan is in a terminal state.
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
)
|
||||
|
||||
plan = self.get_plan(plan_id)
|
||||
|
||||
if plan.is_terminal:
|
||||
raise PlanError(f"Plan {plan_id} is in terminal state and cannot be paused")
|
||||
|
||||
previous_level = plan.automation_level
|
||||
plan.automation_level = AutomationLevel.MANUAL
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name="manual",
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
|
||||
self._commit_plan(plan)
|
||||
self._logger.info(
|
||||
"Plan paused (automation set to manual)",
|
||||
"Plan paused (automation profile set to manual)",
|
||||
plan_id=plan_id,
|
||||
previous_level=previous_level.value,
|
||||
)
|
||||
|
||||
return plan
|
||||
@@ -1175,14 +1144,14 @@ class PlanLifecycleService:
|
||||
def resume_plan(
|
||||
self,
|
||||
plan_id: str,
|
||||
automation_level: AutomationLevel | None = None,
|
||||
automation_profile: str | None = None,
|
||||
) -> Plan:
|
||||
"""Resume auto-progression by restoring a non-MANUAL automation level.
|
||||
"""Resume auto-progression by setting a non-manual automation profile.
|
||||
|
||||
If *automation_level* is not provided, defaults to REVIEW_BEFORE_APPLY
|
||||
If *automation_profile* is not provided, defaults to ``"auto"``
|
||||
so the plan will auto-progress but still pause before apply.
|
||||
|
||||
After restoring the level, calls `auto_progress` to immediately
|
||||
After restoring the profile, calls ``auto_progress`` to immediately
|
||||
advance if the plan is ready.
|
||||
|
||||
Returns:
|
||||
@@ -1192,6 +1161,11 @@ class PlanLifecycleService:
|
||||
NotFoundError: If plan not found.
|
||||
PlanError: If plan is in a terminal state.
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
)
|
||||
|
||||
plan = self.get_plan(plan_id)
|
||||
|
||||
if plan.is_terminal:
|
||||
@@ -1199,52 +1173,18 @@ class PlanLifecycleService:
|
||||
f"Plan {plan_id} is in terminal state and cannot be resumed"
|
||||
)
|
||||
|
||||
level = automation_level or AutomationLevel.REVIEW_BEFORE_APPLY
|
||||
plan.automation_level = level
|
||||
profile_name = automation_profile or "auto"
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=profile_name,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
)
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
|
||||
self._logger.info(
|
||||
"Plan resumed",
|
||||
plan_id=plan_id,
|
||||
automation_level=level.value,
|
||||
automation_profile=profile_name,
|
||||
)
|
||||
|
||||
# Attempt immediate auto-progression
|
||||
return self.auto_progress(plan_id)
|
||||
|
||||
def set_plan_automation_level(self, plan_id: str, level: AutomationLevel) -> Plan:
|
||||
"""Change the automation level for an existing plan.
|
||||
|
||||
Only affects future phase transitions; does not replay past ones.
|
||||
Subplans created after this change will inherit the new level.
|
||||
|
||||
Args:
|
||||
plan_id: The plan ULID.
|
||||
level: The new automation level.
|
||||
|
||||
Returns:
|
||||
The updated Plan.
|
||||
|
||||
Raises:
|
||||
NotFoundError: If plan not found.
|
||||
PlanError: If plan is in a terminal state.
|
||||
"""
|
||||
plan = self.get_plan(plan_id)
|
||||
|
||||
if plan.is_terminal:
|
||||
raise PlanError(
|
||||
f"Plan {plan_id} is in terminal state and cannot change "
|
||||
"automation level"
|
||||
)
|
||||
|
||||
plan.automation_level = level
|
||||
plan.timestamps.updated_at = datetime.now()
|
||||
|
||||
self._commit_plan(plan)
|
||||
self._logger.info(
|
||||
"Plan automation level changed",
|
||||
plan_id=plan_id,
|
||||
level=level.value,
|
||||
)
|
||||
|
||||
return plan
|
||||
|
||||
@@ -98,7 +98,6 @@ def _plan_spec_dict(plan: Any) -> dict[str, object]:
|
||||
"action_name": plan.action_name,
|
||||
"description": plan.description,
|
||||
"definition_of_done": plan.definition_of_done,
|
||||
"automation_level": plan.automation_level.value,
|
||||
"strategy_actor": plan.strategy_actor,
|
||||
"execution_actor": plan.execution_actor,
|
||||
"created_at": plan.timestamps.created_at.isoformat(),
|
||||
@@ -1137,7 +1136,6 @@ def _print_lifecycle_plan(plan: Any, title: str = "Plan") -> None:
|
||||
f"(source: {plan.automation_profile.provenance.value})\n"
|
||||
)
|
||||
|
||||
details += f"[bold]Automation Level:[/bold] {plan.automation_level.value}\n"
|
||||
details += f"[bold]Terminal:[/bold] {'yes' if plan.is_terminal else 'no'}\n"
|
||||
|
||||
# Timestamps
|
||||
@@ -1241,16 +1239,6 @@ def use_action(
|
||||
help="Override the invariant reconciliation actor for this plan",
|
||||
),
|
||||
] = None,
|
||||
automation_level: Annotated[
|
||||
str | None,
|
||||
typer.Option(
|
||||
"--automation-level",
|
||||
help=(
|
||||
"Automation level: manual, review_before_apply, "
|
||||
"or full_automation (defaults to global setting)"
|
||||
),
|
||||
),
|
||||
] = None,
|
||||
fmt: Annotated[
|
||||
str,
|
||||
typer.Option(
|
||||
@@ -1274,7 +1262,6 @@ def use_action(
|
||||
ActionNotAvailableError,
|
||||
)
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
InvariantSource,
|
||||
@@ -1314,19 +1301,6 @@ def use_action(
|
||||
# Get action by name
|
||||
action = service.get_action_by_name(action_name)
|
||||
|
||||
# Parse automation level if provided
|
||||
resolved_automation = None
|
||||
if automation_level:
|
||||
try:
|
||||
resolved_automation = AutomationLevel(automation_level.lower())
|
||||
except ValueError:
|
||||
valid = ", ".join(lv.value for lv in AutomationLevel)
|
||||
console.print(
|
||||
f"[red]Invalid automation level:[/red] {automation_level}. "
|
||||
f"Valid values: {valid}"
|
||||
)
|
||||
raise typer.Abort() from None
|
||||
|
||||
# Build project links from project names
|
||||
project_links = [ProjectLink(project_name=p) for p in all_projects]
|
||||
|
||||
@@ -1343,12 +1317,21 @@ def use_action(
|
||||
action_name=str(action.namespaced_name),
|
||||
project_links=project_links,
|
||||
arguments=arguments if arguments else None,
|
||||
automation_level=resolved_automation,
|
||||
invariants=plan_invariants,
|
||||
)
|
||||
|
||||
# Apply automation profile override if provided
|
||||
if automation_profile:
|
||||
from cleveragents.domain.models.core.automation_profile import (
|
||||
BUILTIN_PROFILES,
|
||||
)
|
||||
|
||||
if automation_profile not in BUILTIN_PROFILES:
|
||||
console.print(
|
||||
f"[red]Invalid automation profile:[/red] {automation_profile}. "
|
||||
f"Available: {', '.join(sorted(BUILTIN_PROFILES))}"
|
||||
)
|
||||
raise typer.Abort()
|
||||
plan.automation_profile = AutomationProfileRef(
|
||||
profile_name=automation_profile,
|
||||
provenance=AutomationProfileProvenance.PLAN,
|
||||
@@ -1777,60 +1760,6 @@ def lifecycle_list_plans(
|
||||
raise typer.Abort() from e
|
||||
|
||||
|
||||
@app.command("set-automation-level")
|
||||
def set_automation_level(
|
||||
plan_id: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Plan ID to change automation level for"),
|
||||
],
|
||||
level: Annotated[
|
||||
str,
|
||||
typer.Argument(
|
||||
help=("Automation level: manual, review_before_apply, or full_automation"),
|
||||
),
|
||||
],
|
||||
) -> None:
|
||||
"""Change the automation level for an existing plan.
|
||||
|
||||
Only affects future phase transitions. Subplans created after
|
||||
this change will inherit the new level.
|
||||
|
||||
Plans in terminal state cannot have their level changed.
|
||||
|
||||
Examples:
|
||||
agents plan set-automation-level 01HXYZ... full_automation
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import AutomationLevel
|
||||
|
||||
try:
|
||||
service = _get_lifecycle_service()
|
||||
|
||||
# Parse the automation level
|
||||
try:
|
||||
parsed_level = AutomationLevel(level.lower())
|
||||
except ValueError:
|
||||
valid = ", ".join(lv.value for lv in AutomationLevel)
|
||||
console.print(
|
||||
f"[red]Invalid automation level:[/red] {level}. Valid values: {valid}"
|
||||
)
|
||||
raise typer.Abort() from None
|
||||
|
||||
plan = service.set_plan_automation_level(plan_id, parsed_level)
|
||||
|
||||
console.print(
|
||||
f"[green]✓[/green] Automation level set to "
|
||||
f"[bold]{parsed_level.value}[/bold] "
|
||||
f"for plan {plan.namespaced_name}"
|
||||
)
|
||||
|
||||
except PlanError as e:
|
||||
console.print(f"[red]Cannot change level:[/red] {e.message}")
|
||||
raise typer.Abort() from e
|
||||
except CleverAgentsError as e:
|
||||
console.print(f"[red]Error:[/red] {e.message}")
|
||||
raise typer.Abort() from e
|
||||
|
||||
|
||||
@app.command("cancel")
|
||||
def cancel_plan(
|
||||
plan_id: Annotated[
|
||||
|
||||
@@ -81,26 +81,14 @@ class Settings(BaseSettings):
|
||||
"mock": "mock-gpt",
|
||||
}
|
||||
|
||||
# Automation level (plan lifecycle) -- legacy; prefer automation_profile
|
||||
default_automation_level: str = Field(
|
||||
default="manual",
|
||||
validation_alias=AliasChoices("CLEVERAGENTS_AUTOMATION_LEVEL"),
|
||||
description=(
|
||||
"Default automation level for new plans: "
|
||||
"'manual', 'review_before_apply', or 'full_automation'"
|
||||
),
|
||||
)
|
||||
|
||||
# Automation profile (supersedes automation_level)
|
||||
# Automation profile (the only automation control)
|
||||
default_automation_profile: str = Field(
|
||||
default="",
|
||||
validation_alias=AliasChoices(
|
||||
"CLEVERAGENTS_AUTOMATION_PROFILE",
|
||||
),
|
||||
description=(
|
||||
"Default automation profile name "
|
||||
"(e.g. 'manual', 'auto', 'full-auto'). "
|
||||
"Overrides default_automation_level when set."
|
||||
"Default automation profile name (e.g. 'manual', 'auto', 'full-auto')."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ Required config keys: ``name``, ``description``
|
||||
|
||||
Optional keys: ``definition_of_done``, ``strategy_actor``,
|
||||
``execution_actor``, ``arguments``, ``invariants``, ``reusable``,
|
||||
``automation_level``, ``metadata``
|
||||
``automation_profile``, ``metadata``
|
||||
|
||||
## Templating
|
||||
|
||||
|
||||
@@ -35,13 +35,11 @@ constraints), ``errored``, or ``cancelled``.
|
||||
| ``CONSTRAINED``| Cannot proceed within constraints (Apply only) |
|
||||
| ``CANCELLED`` | User/system cancelled (any phase) |
|
||||
|
||||
## Automation Levels
|
||||
## Automation Profiles
|
||||
|
||||
| Level | Behavior |
|
||||
|--------------------------|---------------------------------------------|
|
||||
| ``MANUAL`` | Human must approve every transition |
|
||||
| ``REVIEW_BEFORE_APPLY`` | Auto-execute, pause before apply |
|
||||
| ``FULL_AUTOMATION`` | Auto-progress through all phases |
|
||||
Automation behavior is controlled exclusively by ``AutomationProfileRef``,
|
||||
resolved at ``plan use`` time using plan > action > project > global precedence.
|
||||
See ``AutomationProfile`` for threshold semantics.
|
||||
|
||||
## Key Classes
|
||||
|
||||
@@ -49,7 +47,6 @@ constraints), ``errored``, or ``cancelled``.
|
||||
- ``PlanIdentity`` -- plan_id (ULID) + optional name
|
||||
- ``Plan`` -- the core plan model with lifecycle state
|
||||
- ``PlanPhase`` / ``ProcessingState`` -- lifecycle enums
|
||||
- ``AutomationLevel`` -- automation profile enum
|
||||
- ``PlanInvariant`` -- invariant with source provenance
|
||||
- ``ProjectLink`` -- project binding with alias
|
||||
- ``SubplanConfig`` / ``SubplanAttempt`` -- subplan hierarchy
|
||||
@@ -110,20 +107,6 @@ class ProcessingState(StrEnum):
|
||||
CONSTRAINED = "constrained" # Apply cannot proceed within constraints
|
||||
|
||||
|
||||
class AutomationLevel(StrEnum):
|
||||
"""Automation level for plan phase transitions.
|
||||
|
||||
Controls how much human intervention is required during plan execution:
|
||||
- MANUAL: User must explicitly trigger each phase transition
|
||||
- REVIEW_BEFORE_APPLY: Auto strategize+execute, pause before apply
|
||||
- FULL_AUTOMATION: All phases run automatically without human input
|
||||
"""
|
||||
|
||||
MANUAL = "manual"
|
||||
REVIEW_BEFORE_APPLY = "review_before_apply"
|
||||
FULL_AUTOMATION = "full_automation"
|
||||
|
||||
|
||||
class ExecutionMode(StrEnum):
|
||||
"""How subplans should be executed.
|
||||
|
||||
@@ -571,12 +554,6 @@ class Plan(BaseModel):
|
||||
description="Processing state in the current phase",
|
||||
)
|
||||
|
||||
# Automation
|
||||
automation_level: AutomationLevel = Field(
|
||||
AutomationLevel.MANUAL,
|
||||
description="How automated phase transitions should be",
|
||||
)
|
||||
|
||||
# Automation profile - resolved at plan use time and locked
|
||||
automation_profile: AutomationProfileRef | None = Field(
|
||||
default=None,
|
||||
@@ -816,7 +793,6 @@ class Plan(BaseModel):
|
||||
result["automation_profile_source"] = (
|
||||
self.automation_profile.provenance.value
|
||||
)
|
||||
result["automation_level"] = self.automation_level.value
|
||||
if self.project_links:
|
||||
result["projects"] = [
|
||||
{
|
||||
|
||||
@@ -195,10 +195,6 @@ class Session(BaseModel):
|
||||
default_factory=list,
|
||||
description="ULID references to linked plans",
|
||||
)
|
||||
automation_level: str | None = Field(
|
||||
default=None,
|
||||
description="Session-level automation override",
|
||||
)
|
||||
token_usage: SessionTokenUsage = Field(
|
||||
default_factory=SessionTokenUsage,
|
||||
description="Accumulated token usage for this session",
|
||||
@@ -327,7 +323,7 @@ class Session(BaseModel):
|
||||
Matches the output fields from ``agents session show``:
|
||||
|
||||
- ``session_id``, ``actor_name``, ``namespace``, ``message_count``
|
||||
- ``created_at``, ``updated_at``, ``automation_level``
|
||||
- ``created_at``, ``updated_at``
|
||||
- ``recent_messages`` (last 5), ``linked_plan_ids``
|
||||
- ``token_usage`` (input/output/cost), ``metadata``
|
||||
"""
|
||||
@@ -339,8 +335,6 @@ class Session(BaseModel):
|
||||
result["message_count"] = self.message_count
|
||||
result["created_at"] = self.created_at.isoformat()
|
||||
result["updated_at"] = self.updated_at.isoformat()
|
||||
if self.automation_level:
|
||||
result["automation_level"] = self.automation_level
|
||||
if self.messages:
|
||||
recent = self.messages[-5:]
|
||||
result["recent_messages"] = [
|
||||
@@ -364,7 +358,7 @@ class Session(BaseModel):
|
||||
|
||||
- ``schema_version``, ``session_id``, ``actor_name``, ``namespace``
|
||||
- ``messages`` (full history), ``linked_plan_ids``
|
||||
- ``automation_level``, ``token_usage``, ``metadata``
|
||||
- ``token_usage``, ``metadata``
|
||||
- ``created_at``, ``updated_at``, ``checksum`` (SHA-256)
|
||||
"""
|
||||
messages_data = [
|
||||
@@ -386,7 +380,6 @@ class Session(BaseModel):
|
||||
"namespace": self.namespace,
|
||||
"messages": messages_data,
|
||||
"linked_plan_ids": list(self.linked_plan_ids),
|
||||
"automation_level": self.automation_level,
|
||||
"token_usage": {
|
||||
"input_tokens": self.token_usage.input_tokens,
|
||||
"output_tokens": self.token_usage.output_tokens,
|
||||
|
||||
@@ -574,9 +574,6 @@ class LifecyclePlanModel(Base): # type: ignore[misc]
|
||||
|
||||
# Policy / profile
|
||||
automation_profile = Column(Text, nullable=True)
|
||||
# Legacy: automation_level is retained for backward compatibility.
|
||||
# New code should use automation_profile. Will be removed in A6.legacy.
|
||||
automation_level = Column(String(30), nullable=False, default="manual")
|
||||
|
||||
# Behavior
|
||||
reusable = Column(Boolean, nullable=False, default=True)
|
||||
@@ -657,10 +654,6 @@ class LifecyclePlanModel(Base): # type: ignore[misc]
|
||||
"'applied', 'constrained')",
|
||||
name="ck_v3_plans_state",
|
||||
),
|
||||
CheckConstraint(
|
||||
"automation_level IN ('manual', 'review_before_apply', 'full_automation')",
|
||||
name="ck_v3_plans_automation",
|
||||
),
|
||||
Index("ix_v3_plans_phase", "phase"),
|
||||
Index("ix_v3_plans_state", "processing_state"),
|
||||
Index("ix_v3_plans_parent", "parent_plan_id"),
|
||||
@@ -694,7 +687,6 @@ class LifecyclePlanModel(Base): # type: ignore[misc]
|
||||
A ``Plan`` domain instance.
|
||||
"""
|
||||
from cleveragents.domain.models.core.plan import (
|
||||
AutomationLevel,
|
||||
AutomationProfileProvenance,
|
||||
AutomationProfileRef,
|
||||
InvariantSource,
|
||||
@@ -787,9 +779,6 @@ class LifecyclePlanModel(Base): # type: ignore[misc]
|
||||
definition_of_done=cast("str | None", self.definition_of_done),
|
||||
phase=phase_enum,
|
||||
processing_state=state_enum,
|
||||
automation_level=AutomationLevel(
|
||||
cast(str, self.automation_level or "manual")
|
||||
),
|
||||
automation_profile=automation_profile_ref,
|
||||
strategy_actor=cast("str | None", self.strategy_actor),
|
||||
execution_actor=cast("str | None", self.execution_actor),
|
||||
@@ -887,11 +876,6 @@ class LifecyclePlanModel(Base): # type: ignore[misc]
|
||||
apply_actor=getattr(plan, "apply_actor", None),
|
||||
estimation_actor=getattr(plan, "estimation_actor", None),
|
||||
invariant_actor=getattr(plan, "invariant_actor", None),
|
||||
automation_level=(
|
||||
plan.automation_level.value
|
||||
if hasattr(plan.automation_level, "value")
|
||||
else plan.automation_level
|
||||
),
|
||||
automation_profile=automation_profile_json,
|
||||
reusable=plan.reusable,
|
||||
read_only=plan.read_only,
|
||||
|
||||
Reference in New Issue
Block a user