From 089bee4846d700d564906eb39db4740835971b13 Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Thu, 19 Feb 2026 05:18:17 +0000 Subject: [PATCH] refactor(automation): remove automation_level legacy fields --- .../versions/a6_002_drop_automation_level.py | 46 +++++ benchmarks/automation_legacy_cleanup_bench.py | 58 ++++++ .../automation_profile_resolution_bench.py | 21 --- benchmarks/cli_format_bench.py | 2 - benchmarks/db_migration_plans_bench.py | 3 - benchmarks/legacy_removal_bench.py | 1 - benchmarks/plan_cli_bench.py | 2 - benchmarks/plan_repository_bench.py | 2 - benchmarks/uow_lifecycle_bench.py | 2 - features/automation_level_removed.feature | 30 ++++ features/automation_profile_service.feature | 33 ---- .../plan_lifecycle_commands_coverage.feature | 34 +--- features/plan_model_coverage.feature | 1 - features/session_model.feature | 5 - .../steps/action_repository_coverage_steps.py | 1 - features/steps/automation_levels_steps.py | 167 +++++++++++------- .../steps/automation_profile_service_steps.py | 28 --- features/steps/cli_output_formats_steps.py | 2 - .../database_models_coverage_boost_steps.py | 11 -- ...atabase_models_lifecycle_coverage_steps.py | 19 +- features/steps/legacy_plan_removal_steps.py | 2 - .../steps/plan_cli_coverage_boost_steps.py | 2 - .../steps/plan_cli_spec_alignment_steps.py | 2 - .../plan_lifecycle_commands_coverage_steps.py | 118 ++----------- .../steps/plan_lifecycle_coverage_steps.py | 3 - ..._lifecycle_service_coverage_boost_steps.py | 2 - features/steps/plan_model_steps.py | 23 ++- features/steps/plan_repository_steps.py | 2 - .../repositories_coverage_boost_steps.py | 2 - ...ositories_error_handling_coverage_steps.py | 1 - features/steps/session_model_steps.py | 4 +- features/steps/uow_lifecycle_steps.py | 2 - implementation_plan.md | 36 ++-- robot/automation_level_removed.robot | 23 +++ robot/automation_profile_service.robot | 6 - robot/helper_automation_profile_service.py | 16 -- robot/helper_cli_formats.py | 2 - robot/helper_db_lifecycle_models.py | 3 - robot/helper_plan_cli_spec.py | 2 - robot/helper_plan_lifecycle_v3.py | 38 ++-- robot/helper_plan_phase_migration.py | 3 - robot/helper_plan_repository.py | 2 - robot/helper_uow_lifecycle.py | 2 - robot/legacy_plan_removal_helper.py | 3 +- .../services/automation_profile_service.py | 62 +------ .../services/plan_lifecycle_service.py | 128 ++++---------- src/cleveragents/cli/commands/plan.py | 91 ++-------- src/cleveragents/config/settings.py | 16 +- src/cleveragents/domain/models/core/action.py | 2 +- src/cleveragents/domain/models/core/plan.py | 32 +--- .../domain/models/core/session.py | 11 +- .../infrastructure/database/models.py | 16 -- 52 files changed, 394 insertions(+), 731 deletions(-) create mode 100644 alembic/versions/a6_002_drop_automation_level.py create mode 100644 benchmarks/automation_legacy_cleanup_bench.py create mode 100644 features/automation_level_removed.feature create mode 100644 robot/automation_level_removed.robot diff --git a/alembic/versions/a6_002_drop_automation_level.py b/alembic/versions/a6_002_drop_automation_level.py new file mode 100644 index 000000000..ab1b8ebc6 --- /dev/null +++ b/alembic/versions/a6_002_drop_automation_level.py @@ -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')", + ) diff --git a/benchmarks/automation_legacy_cleanup_bench.py b/benchmarks/automation_legacy_cleanup_bench.py new file mode 100644 index 000000000..8e0cf8cd7 --- /dev/null +++ b/benchmarks/automation_legacy_cleanup_bench.py @@ -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) diff --git a/benchmarks/automation_profile_resolution_bench.py b/benchmarks/automation_profile_resolution_bench.py index 83ce3d4fd..0514fed74 100644 --- a/benchmarks/automation_profile_resolution_bench.py +++ b/benchmarks/automation_profile_resolution_bench.py @@ -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.""" diff --git a/benchmarks/cli_format_bench.py b/benchmarks/cli_format_bench.py index 6ba70958c..d148b0dd4 100644 --- a/benchmarks/cli_format_bench.py +++ b/benchmarks/cli_format_bench.py @@ -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")], diff --git a/benchmarks/db_migration_plans_bench.py b/benchmarks/db_migration_plans_bench.py index cffef8a2c..583e50f7d 100644 --- a/benchmarks/db_migration_plans_bench.py +++ b/benchmarks/db_migration_plans_bench.py @@ -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, diff --git a/benchmarks/legacy_removal_bench.py b/benchmarks/legacy_removal_bench.py index 324e90066..52a10d3f8 100644 --- a/benchmarks/legacy_removal_bench.py +++ b/benchmarks/legacy_removal_bench.py @@ -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: diff --git a/benchmarks/plan_cli_bench.py b/benchmarks/plan_cli_bench.py index b77368cf2..a59d03bb6 100644 --- a/benchmarks/plan_cli_bench.py +++ b/benchmarks/plan_cli_bench.py @@ -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"], diff --git a/benchmarks/plan_repository_bench.py b/benchmarks/plan_repository_bench.py index d4aa763c2..02a430649 100644 --- a/benchmarks/plan_repository_bench.py +++ b/benchmarks/plan_repository_bench.py @@ -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), diff --git a/benchmarks/uow_lifecycle_bench.py b/benchmarks/uow_lifecycle_bench.py index 0d1fa7918..16df2ac4d 100644 --- a/benchmarks/uow_lifecycle_bench.py +++ b/benchmarks/uow_lifecycle_bench.py @@ -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={}, diff --git a/features/automation_level_removed.feature b/features/automation_level_removed.feature new file mode 100644 index 000000000..cd0e97de3 --- /dev/null +++ b/features/automation_level_removed.feature @@ -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" diff --git a/features/automation_profile_service.feature b/features/automation_profile_service.feature index 7a455540b..c9b520c2d 100644 --- a/features/automation_profile_service.feature +++ b/features/automation_profile_service.feature @@ -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 diff --git a/features/plan_lifecycle_commands_coverage.feature b/features/plan_lifecycle_commands_coverage.feature index 8b057aa27..9cbbdd09a 100644 --- a/features/plan_lifecycle_commands_coverage.feature +++ b/features/plan_lifecycle_commands_coverage.feature @@ -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 # =================================================================== diff --git a/features/plan_model_coverage.feature b/features/plan_model_coverage.feature index 47da347a2..5e64efb22 100644 --- a/features/plan_model_coverage.feature +++ b/features/plan_model_coverage.feature @@ -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" diff --git a/features/session_model.feature b/features/session_model.feature index 8108110d9..d9925dd16 100644 --- a/features/session_model.feature +++ b/features/session_model.feature @@ -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 diff --git a/features/steps/action_repository_coverage_steps.py b/features/steps/action_repository_coverage_steps.py index c7474ab9a..f7c75bf38 100644 --- a/features/steps/action_repository_coverage_steps.py +++ b/features/steps/action_repository_coverage_steps.py @@ -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", diff --git a/features/steps/automation_levels_steps.py b/features/steps/automation_levels_steps.py index da29d0045..9c9eb34a1 100644 --- a/features/steps/automation_levels_steps.py +++ b/features/steps/automation_levels_steps.py @@ -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}'" ) diff --git a/features/steps/automation_profile_service_steps.py b/features/steps/automation_profile_service_steps.py index 339beb048..7609330c7 100644 --- a/features/steps/automation_profile_service_steps.py +++ b/features/steps/automation_profile_service_steps.py @@ -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 # ------------------------------------------------------------------- diff --git a/features/steps/cli_output_formats_steps.py b/features/steps/cli_output_formats_steps.py index 7f2f02c17..efbcc4e21 100644 --- a/features/steps/cli_output_formats_steps.py +++ b/features/steps/cli_output_formats_steps.py @@ -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")], diff --git a/features/steps/database_models_coverage_boost_steps.py b/features/steps/database_models_coverage_boost_steps.py index 45bd4f27c..8050694dc 100644 --- a/features/steps/database_models_coverage_boost_steps.py +++ b/features/steps/database_models_coverage_boost_steps.py @@ -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"}, diff --git a/features/steps/database_models_lifecycle_coverage_steps.py b/features/steps/database_models_lifecycle_coverage_steps.py index bd0a1deda..6ede612d1 100644 --- a/features/steps/database_models_lifecycle_coverage_steps.py +++ b/features/steps/database_models_lifecycle_coverage_steps.py @@ -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 # --------------------------------------------------------------------------- diff --git a/features/steps/legacy_plan_removal_steps.py b/features/steps/legacy_plan_removal_steps.py index 9276fb69d..9fce487a4 100644 --- a/features/steps/legacy_plan_removal_steps.py +++ b/features/steps/legacy_plan_removal_steps.py @@ -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) diff --git a/features/steps/plan_cli_coverage_boost_steps.py b/features/steps/plan_cli_coverage_boost_steps.py index fe4e3c73d..08a4811d1 100644 --- a/features/steps/plan_cli_coverage_boost_steps.py +++ b/features/steps/plan_cli_coverage_boost_steps.py @@ -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, ) diff --git a/features/steps/plan_cli_spec_alignment_steps.py b/features/steps/plan_cli_spec_alignment_steps.py index 92c822cf0..31843fd5d 100644 --- a/features/steps/plan_cli_spec_alignment_steps.py +++ b/features/steps/plan_cli_spec_alignment_steps.py @@ -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 [], diff --git a/features/steps/plan_lifecycle_commands_coverage_steps.py b/features/steps/plan_lifecycle_commands_coverage_steps.py index d8b7a6ea5..a94177f53 100644 --- a/features/steps/plan_lifecycle_commands_coverage_steps.py +++ b/features/steps/plan_lifecycle_commands_coverage_steps.py @@ -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 ( diff --git a/features/steps/plan_lifecycle_coverage_steps.py b/features/steps/plan_lifecycle_coverage_steps.py index 2c64fccb3..ba50807b7 100644 --- a/features/steps/plan_lifecycle_coverage_steps.py +++ b/features/steps/plan_lifecycle_coverage_steps.py @@ -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] diff --git a/features/steps/plan_lifecycle_service_coverage_boost_steps.py b/features/steps/plan_lifecycle_service_coverage_boost_steps.py index cc0c67787..701e06fd7 100644 --- a/features/steps/plan_lifecycle_service_coverage_boost_steps.py +++ b/features/steps/plan_lifecycle_service_coverage_boost_steps.py @@ -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 diff --git a/features/steps/plan_model_steps.py b/features/steps/plan_model_steps.py index a5ed88606..4738a6628 100644 --- a/features/steps/plan_model_steps.py +++ b/features/steps/plan_model_steps.py @@ -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, diff --git a/features/steps/plan_repository_steps.py b/features/steps/plan_repository_steps.py index e168af6ee..ddd1b77bf 100644 --- a/features/steps/plan_repository_steps.py +++ b/features/steps/plan_repository_steps.py @@ -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 [], diff --git a/features/steps/repositories_coverage_boost_steps.py b/features/steps/repositories_coverage_boost_steps.py index 65f39c70b..60ec0e563 100644 --- a/features/steps/repositories_coverage_boost_steps.py +++ b/features/steps/repositories_coverage_boost_steps.py @@ -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( diff --git a/features/steps/repositories_error_handling_coverage_steps.py b/features/steps/repositories_error_handling_coverage_steps.py index 99d3233ba..1fe40fae2 100644 --- a/features/steps/repositories_error_handling_coverage_steps.py +++ b/features/steps/repositories_error_handling_coverage_steps.py @@ -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, diff --git a/features/steps/session_model_steps.py b/features/steps/session_model_steps.py index 6d7ebbe42..0c479b139 100644 --- a/features/steps/session_model_steps.py +++ b/features/steps/session_model_steps.py @@ -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 diff --git a/features/steps/uow_lifecycle_steps.py b/features/steps/uow_lifecycle_steps.py index bd01e9ff3..61107cc8a 100644 --- a/features/steps/uow_lifecycle_steps.py +++ b/features/steps/uow_lifecycle_steps.py @@ -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, diff --git a/implementation_plan.md b/implementation_plan.md index 79446cbfc..f06896ae4 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -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). diff --git a/robot/automation_level_removed.robot b/robot/automation_level_removed.robot new file mode 100644 index 000000000..27f87cf9a --- /dev/null +++ b/robot/automation_level_removed.robot @@ -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 diff --git a/robot/automation_profile_service.robot b/robot/automation_profile_service.robot index 015182b00..e5b586766 100644 --- a/robot/automation_profile_service.robot +++ b/robot/automation_profile_service.robot @@ -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} diff --git a/robot/helper_automation_profile_service.py b/robot/helper_automation_profile_service.py index d70957128..57d83481c 100644 --- a/robot/helper_automation_profile_service.py +++ b/robot/helper_automation_profile_service.py @@ -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, } diff --git a/robot/helper_cli_formats.py b/robot/helper_cli_formats.py index 2e57e3f79..946e40d33 100644 --- a/robot/helper_cli_formats.py +++ b/robot/helper_cli_formats.py @@ -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")], diff --git a/robot/helper_db_lifecycle_models.py b/robot/helper_db_lifecycle_models.py index 396361e49..8d7211382 100644 --- a/robot/helper_db_lifecycle_models.py +++ b/robot/helper_db_lifecycle_models.py @@ -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 diff --git a/robot/helper_plan_cli_spec.py b/robot/helper_plan_cli_spec.py index 137c3c7d0..6c17e4fe3 100644 --- a/robot/helper_plan_cli_spec.py +++ b/robot/helper_plan_cli_spec.py @@ -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()), diff --git a/robot/helper_plan_lifecycle_v3.py b/robot/helper_plan_lifecycle_v3.py index c8f71a21b..851923788 100644 --- a/robot/helper_plan_lifecycle_v3.py +++ b/robot/helper_plan_lifecycle_v3.py @@ -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") diff --git a/robot/helper_plan_phase_migration.py b/robot/helper_plan_phase_migration.py index a25549bd2..d02464d05 100644 --- a/robot/helper_plan_phase_migration.py +++ b/robot/helper_plan_phase_migration.py @@ -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( diff --git a/robot/helper_plan_repository.py b/robot/helper_plan_repository.py index 279341b84..e6d2d0a14 100644 --- a/robot/helper_plan_repository.py +++ b/robot/helper_plan_repository.py @@ -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), diff --git a/robot/helper_uow_lifecycle.py b/robot/helper_uow_lifecycle.py index 8e2d56514..178ef767a 100644 --- a/robot/helper_uow_lifecycle.py +++ b/robot/helper_uow_lifecycle.py @@ -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={}, diff --git a/robot/legacy_plan_removal_helper.py b/robot/legacy_plan_removal_helper.py index cd4e03708..84c143b36 100644 --- a/robot/legacy_plan_removal_helper.py +++ b/robot/legacy_plan_removal_helper.py @@ -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) diff --git a/src/cleveragents/application/services/automation_profile_service.py b/src/cleveragents/application/services/automation_profile_service.py index a1e069f6d..7cf3ac464 100644 --- a/src/cleveragents/application/services/automation_profile_service.py +++ b/src/cleveragents/application/services/automation_profile_service.py @@ -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]: diff --git a/src/cleveragents/application/services/plan_lifecycle_service.py b/src/cleveragents/application/services/plan_lifecycle_service.py index 08891c28c..efe6773f1 100644 --- a/src/cleveragents/application/services/plan_lifecycle_service.py +++ b/src/cleveragents/application/services/plan_lifecycle_service.py @@ -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 diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index 172d8f780..0a1707bc2 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -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[ diff --git a/src/cleveragents/config/settings.py b/src/cleveragents/config/settings.py index 737a7a226..a635c0bc0 100644 --- a/src/cleveragents/config/settings.py +++ b/src/cleveragents/config/settings.py @@ -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')." ), ) diff --git a/src/cleveragents/domain/models/core/action.py b/src/cleveragents/domain/models/core/action.py index 1734b9080..bf7915646 100644 --- a/src/cleveragents/domain/models/core/action.py +++ b/src/cleveragents/domain/models/core/action.py @@ -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 diff --git a/src/cleveragents/domain/models/core/plan.py b/src/cleveragents/domain/models/core/plan.py index bf98c6a4c..ab628e433 100644 --- a/src/cleveragents/domain/models/core/plan.py +++ b/src/cleveragents/domain/models/core/plan.py @@ -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"] = [ { diff --git a/src/cleveragents/domain/models/core/session.py b/src/cleveragents/domain/models/core/session.py index 22cfec637..39710127e 100644 --- a/src/cleveragents/domain/models/core/session.py +++ b/src/cleveragents/domain/models/core/session.py @@ -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, diff --git a/src/cleveragents/infrastructure/database/models.py b/src/cleveragents/infrastructure/database/models.py index bc5b584f6..e481c098b 100644 --- a/src/cleveragents/infrastructure/database/models.py +++ b/src/cleveragents/infrastructure/database/models.py @@ -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,