fix(acms): restore all non-ACMS files to master to fix unit_tests CI failure
CI / benchmark-publish (pull_request) Has been skipped
CI / push-validation (pull_request) Successful in 34s
CI / helm (pull_request) Successful in 48s
CI / build (pull_request) Successful in 54s
CI / lint (pull_request) Successful in 58s
CI / quality (pull_request) Successful in 1m21s
CI / benchmark-regression (pull_request) Failing after 1m21s
CI / typecheck (pull_request) Successful in 1m34s
CI / security (pull_request) Successful in 1m40s
CI / unit_tests (pull_request) Failing after 1m38s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 3m56s
CI / e2e_tests (pull_request) Successful in 4m3s
CI / status-check (pull_request) Failing after 3s

The previous rebase accidentally included many non-ACMS changes from other
branches. This commit restores all non-ACMS files to their master versions,
leaving only the 7 ACMS-specific files changed:
- src/cleveragents/acms/index.py (new)
- src/cleveragents/acms/__init__.py (modified)
- features/acms/index_data_model_and_traversal.feature (new)
- features/steps/acms_index_data_model_traversal_steps.py (new)
- features/environment.py (modified)
- CHANGELOG.md (modified)
- CONTRIBUTORS.md (modified)

Closes #9579
This commit is contained in:
2026-05-05 03:52:03 +00:00
parent af04db659c
commit 35497bc893
44 changed files with 942 additions and 1865 deletions
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv=${{ env.UV_VERSION }} nox
pip install -q uv==${{ env.UV_VERSION }} nox
- name: Cache uv packages
uses: actions/cache@v3
@@ -126,7 +126,7 @@ jobs:
- name: Install uv and nox
run: |
pip install -q uv=${{ env.UV_VERSION }} nox
pip install -q uv==${{ env.UV_VERSION }} nox
- name: Cache uv packages
uses: actions/cache@v3
+5 -3
View File
@@ -327,11 +327,13 @@ jobs:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
- name: Upload E2E tests log artifact
if: always()
if: failure()
uses: actions/upload-artifact@v3
with:
name: ci-logs-e2e-tests
path: build/nox-e2e-tests-output.log
path: |
build/nox-e2e-tests-output.log
build/reports/robot-e2e/
retention-days: 30
coverage:
@@ -444,7 +446,7 @@ jobs:
needs: [lint, typecheck, security, quality, unit_tests]
runs-on: docker
container:
image: ${{vars.docker_prefix}}docker:dind
image: docker:dind
options: --privileged
steps:
- name: Start Docker daemon and install dependencies
+1 -1
View File
@@ -92,7 +92,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install -U pip
python -m pip install asv virtualenv uv=${{ env.UV_VERSION }} nox
python -m pip install asv virtualenv uv==${{ env.UV_VERSION }} nox
- name: Sync prior benchmark results from S3
env:
+1 -1
View File
@@ -80,7 +80,7 @@ jobs:
- name: Run quality gates script
run: |
python scripts/check-quality-gates.py --coverage-min 97 --complexity-max F || echo "Quality gates script not found or failed, skipping..."
python scripts/check-quality-gates.py --coverage-min 96.5 --complexity-max F || echo "Quality gates script not found or failed, skipping..."
- name: Generate quality trend data
run: |
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
build-docker:
runs-on: docker
container:
image: ${{vars.docker_prefix}}docker:dind
image: docker:dind
options: --privileged
needs: [build-wheel]
steps:
+50 -65
View File
@@ -1480,35 +1480,37 @@ def process_data(self, data: list[str], threshold: int) -> None:
---
## Workflow Choice: Legacy vs. v3 Plan Lifecycle
## V3 Plan Lifecycle (Legacy Workflow Removed)
The CleverAgents CLI supports two distinct plan workflow systems. **These systems are mutually
exclusive and cannot be mixed.** Choosing one means committing to it for the entire lifecycle
of a plan.
As of v3.5.0, the CleverAgents CLI supports **only the V3 Plan Lifecycle workflow**. All legacy
commands (`agents tell`, `agents build`, `agents continue`, `agents current`, `agents cd`,
`agents new`) have been **permanently removed**.
### The Two Systems
### The V3 Plan Lifecycle Workflow
| System | Commands | Identifier Format | Storage |
|--------|----------|-------------------|---------|
| **Legacy** (deprecated) | `agents tell`, `agents build`, `agents apply <name>` | Human-readable names (e.g., `"64-bit port plan"`) | `PlanService` (legacy) |
| **v3 Lifecycle** (authoritative) | `agents plan use`, `agents plan execute <PLAN_ID>`, `agents plan apply <PLAN_ID>` | ULIDs (e.g., `01HXM8C2ZK4Q7C2B3F2R4VYV6J`) | `PlanLifecycleService` |
The v3 plan lifecycle is the authoritative workflow per `docs/specification.md`. All development
uses v3 commands exclusively:
### Why They Cannot Be Mixed
| Operation | Command | Notes |
|-----------|---------|-------|
| **Create a plan** | `agents plan use <action> <project>` | Returns a ULID identifier |
| **Execute** | `agents plan execute <PLAN_ID>` | Strategize + Execute phases |
| **Apply changes** | `agents plan apply <PLAN_ID>` | Execute → Apply phase transition |
| **List plans** | `agents plan list` | Shows ULID-based plans |
| **Show details** | `agents plan status <PLAN_ID>` | Shows current phase and metrics |
| **Decision tree** | `agents plan tree <PLAN_ID>` | Shows decision tree with rationales |
| **Rollback** | `agents plan rollback <PLAN_ID> <checkpoint>` | Revert to previous checkpoint |
| **Correct decision** | `agents plan correct <PLAN_ID> <decision-id>` | Selective subtree recomputation |
The two systems use **completely separate storage backends**. A plan created with a legacy
command (`agents tell`) exists only in the legacy `PlanService` storage. It is invisible to
v3 commands, which exclusively query `PlanLifecycleService`. Attempting to reference a legacy
plan name with a v3 command will always fail — not because the plan doesn't exist, but because
v3 commands look in a different storage system.
### Identifier Format
Additionally, v3 commands require **ULID identifiers** (26-character Crockford base32 strings).
Passing a human-readable name to `agents plan execute` or `agents plan apply` will be rejected
with an explicit error message explaining the incompatibility.
V3 uses **ULID identifiers** (26-character Crockford base32 strings, e.g., `01HXM8C2ZK4Q7C2B3F2R4VYV6J`)
for reliable plan tracking across sessions, enabling checkpoint support, decision correction,
and hierarchical subplan management.
### The v3 Workflow (Recommended)
### V3 Workflow Example
The v3 plan lifecycle is the authoritative workflow per `docs/specification.md`. All new
development should use v3 commands exclusively:
Create and execute a plan using the V3 lifecycle:
```bash
# Step 1: Create a v3 plan from an action template
@@ -1518,55 +1520,38 @@ agents plan use local/my-action my-project
# Step 2: Execute the plan (strategize + execute phases)
agents plan execute 01HXM8C2ZK4Q7C2B3F2R4VYV6J
# Step 3: Apply the changes
# Step 3: Review the decision tree
agents plan tree 01HXM8C2ZK4Q7C2B3F2R4VYV6J
# Step 4: Apply the changes
agents plan apply 01HXM8C2ZK4Q7C2B3F2R4VYV6J
```
### The Legacy Workflow (Deprecated)
### Legacy Workflow Removed
Legacy commands are deprecated and will be removed in a future version. They emit deprecation
warnings when used. If you must use legacy commands, use them exclusively — do not attempt to
mix them with v3 commands:
As of v3.5.0, **all legacy workflow commands have been permanently removed**:
- `agents tell` (create plan)
- `agents build` (execute plan)
- `agents apply <name>` (legacy apply)
- `agents current` (show current plan)
- `agents continue` (continue execution)
- `agents cd` (change plan context)
- `agents new` (create new plan)
```bash
# Legacy workflow (deprecated — do not mix with v3 commands)
agents tell -n "my-plan" "Devise a plan to..."
agents build
agents apply my-plan
```
These commands are no longer available. If you have legacy workflows that depend on these
commands, you must migrate to the V3 workflow.
### Migration from Legacy to v3
### Migrating from Legacy to V3
There is no automatic migration path. Legacy plans cannot be converted to v3 plans. To migrate:
For detailed step-by-step migration instructions, see `docs/Legacy_to_V3_Guide.md`. The guide
includes:
- Side-by-side command comparison
- Migration examples
- Troubleshooting common errors
- Resource references
1. Identify what the legacy plan was intended to accomplish.
2. Create a new v3 plan using `agents plan use <action> <project>`.
3. Use `agents plan execute <PLAN_ID>` and `agents plan apply <PLAN_ID>` for subsequent steps.
### Error Messages
When a non-ULID identifier is passed to a v3 command, the CLI will display an explicit error
explaining the workflow incompatibility:
```
Error: Plan 'my-legacy-plan' not found.
The v3 plan lifecycle expects a ULID identifier
(e.g., 01HXM8C2ZK4Q7C2B3F2R4VYV6J), not a plan name.
Possible causes:
1. You created a plan with 'agents tell' (legacy workflow) and are
trying to reference it with a v3 command. These workflows are
incompatible and cannot be mixed — legacy plans exist only in the
legacy storage system and are invisible to v3 commands.
2. You referenced the wrong plan ID.
To use the v3 workflow:
- Run 'agents plan use <action> <project>' to create a v3 plan
(this returns a ULID you can use with subsequent commands).
- Run 'agents plan execute <PLAN_ID>' to execute it.
- Run 'agents plan apply <PLAN_ID>' to apply changes.
Legacy commands ('agents tell', 'agents build') operate in a separate
system and cannot be mixed with v3 commands.
```
Key migration points:
1. Legacy plans used human-readable names; V3 uses ULID identifiers
2. No automatic migration path exists; legacy plans must be recreated with V3 commands
3. The V3 workflow provides superior capabilities: decision trees, checkpoints, rollback, and
selective correction
+38 -1
View File
@@ -1,4 +1,4 @@
Feature: Actor context remove, export, and import commands
Feature: Actor context clear, remove, export, and import commands
As a CleverAgents user
I want to manage actor contexts via the CLI
So that I can remove, export, and import conversation contexts
@@ -6,6 +6,43 @@ Feature: Actor context remove, export, and import commands
Background:
Given a temporary context directory for actor context tests
# ── context clear ─────────────────────────────────────────
Scenario: Clear a named actor context
Given an actor context named "docs" exists with messages
When I run actor context clear "docs" with --yes
Then the actor context clear command should succeed
And the context "docs" should exist
And the context "docs" should be empty
Scenario: Clear all actor contexts
Given an actor context named "docs" exists with messages
And an actor context named "notes" exists with messages
When I run actor context clear --all with --yes
Then the actor context clear command should succeed
And all actor contexts should be empty
Scenario: Clear non-existent context fails
When I run actor context clear "ghost" with --yes
Then the actor context clear command should fail with exit code 1
Scenario: Clear requires NAME or --all
When I run actor context clear without name or all
Then the actor context clear command should fail with exit code 1
Scenario: Clear rejects NAME with --all
When I run actor context clear "docs" with --all
Then the actor context clear command should fail with exit code 1
Scenario: Clear outputs JSON format
Given an actor context named "docs" exists with messages
When I run actor context clear "docs" with --yes and format "json"
Then the actor context clear command should succeed
And the output should contain valid JSON with key "context_cleared"
And the output JSON key "context_cleared" should contain keys "items, storage"
And the output should contain valid JSON with key "retention"
And the output JSON key "retention" should contain keys "context, files"
# ── context remove ─────────────────────────────────────────
Scenario: Remove a named actor context
-10
View File
@@ -102,7 +102,6 @@ Feature: Auto Debug CLI Command Coverage
Then the command should exit with code 0
And the output should contain "Could not generate fix"
# Edge case: Long error message truncation
Scenario: Auto debug run command truncates long error messages
Given I have an initialized project for auto_debug
And the build will always fail with error "This is a very long error message that exceeds eighty characters and should be truncated in the display output"
@@ -111,7 +110,6 @@ Feature: Auto Debug CLI Command Coverage
And the output should contain "..."
And the output should contain "Build failed after"
# Edge case: Minimum max_attempts boundary
Scenario: Auto debug run command with minimum max_attempts of 1
Given I have an initialized project for auto_debug
And the build will always fail with error "Single attempt failure"
@@ -120,7 +118,6 @@ Feature: Auto Debug CLI Command Coverage
And the output should contain "Attempt 1/1"
And the output should contain "Build failed after 1 attempt"
# Edge case: Multiple sequential failures before success
Scenario: Auto debug run command succeeds after multiple failures
Given I have an initialized project for auto_debug
And the build will fail 2 times then succeed with 3 changes
@@ -130,7 +127,6 @@ Feature: Auto Debug CLI Command Coverage
And the output should contain "Build successful"
And the output should contain "Generated 3 change(s)"
# Edge case: Short error message (no truncation)
Scenario: Auto debug run command shows short error without truncation
Given I have an initialized project for auto_debug
And the build will always fail with error "Short error"
@@ -139,7 +135,6 @@ Feature: Auto Debug CLI Command Coverage
And the output should contain "Short error"
And the output should not contain "Short error..."
# Edge case: PlanError re-raised from within build loop
Scenario: Auto debug run command re-raises PlanError from build
Given I have an initialized project for auto_debug
And the build will raise PlanError after one attempt
@@ -147,7 +142,6 @@ Feature: Auto Debug CLI Command Coverage
Then the command should be aborted
And the output should contain "Plan Error"
# Edge case: CleverAgentsError re-raised from within build loop
Scenario: Auto debug run command re-raises CleverAgentsError from build
Given I have an initialized project for auto_debug
And the build will raise CleverAgentsError after one attempt
@@ -155,7 +149,6 @@ Feature: Auto Debug CLI Command Coverage
Then the command should be aborted
And the output should contain "Error:"
# Edge case: auto_debug_command with custom max_attempts
Scenario: Auto debug command with custom max_attempts of 5
Given I have an initialized project for auto_debug
And the plan service auto_debug_build will fail
@@ -163,7 +156,6 @@ Feature: Auto Debug CLI Command Coverage
Then the auto_debug_command should return success False
And the attempts made should be 5
# Edge case: auto_debug_command with max_attempts of 1
Scenario: Auto debug command with minimum max_attempts of 1
Given I have an initialized project for auto_debug
And the plan service auto_debug_build will fail
@@ -171,7 +163,6 @@ Feature: Auto Debug CLI Command Coverage
Then the auto_debug_command should return success False
And the attempts made should be 1
# Edge case: Successful build on exact last attempt
Scenario: Auto debug run command succeeds on the last attempt
Given I have an initialized project for auto_debug
And the build will fail 2 times then succeed with 1 changes
@@ -179,7 +170,6 @@ Feature: Auto Debug CLI Command Coverage
Then the command should exit with code 0
And the output should contain "Build succeeded after 3 attempt"
# Edge case: Multiple PlanError details
Scenario: Auto debug run command handles PlanError with multiple details
Given I have an initialized project for auto_debug
And the plan service will raise a PlanError with multiple details
-27
View File
@@ -33,29 +33,6 @@ Feature: CLI Commands Full Coverage
When I run context list
Then the context list should execute
Scenario: Plan tell command
Given I have a temporary test directory
When I run plan tell with instruction "test plan"
Then the plan tell should execute
Scenario: Plan build command
Given I have a temporary test directory
And I have an existing plan
When I run plan build
Then the plan build should execute
Scenario: Plan show command
Given I have a temporary test directory
And I have an existing plan
When I run plan show with plan id
Then the plan show should execute
Scenario: Plan delete command
Given I have a temporary test directory
And I have an existing plan
When I run plan delete with plan id
Then the plan delete should execute
Scenario: Project init command
Given I have a temporary test directory
When I run project init
@@ -77,10 +54,6 @@ Feature: CLI Commands Full Coverage
When I run context command with help
Then the context help should display
Scenario: Plan command group help
When I run plan command with help
Then the plan help should display
Scenario: Project command group help
When I run project command with help
Then the project help should display
+4 -4
View File
@@ -629,9 +629,9 @@ Feature: Consolidated Config
@tdd_issue @tdd_issue_4227
Scenario: Coverage threshold is at least 97 percent in noxfile
Scenario: Coverage threshold is at least 96.5 percent in noxfile
Given the noxfile py is loaded for coverage check
Then the noxfile should contain a fail-under threshold of at least 97
Then the noxfile should contain a fail-under threshold of at least 96.5
Scenario: Coverage report session exists in noxfile
@@ -665,7 +665,7 @@ Feature: Consolidated Config
Then the ci workflow should reference nox coverage_report session
Scenario: Nightly workflow uses at least 97 percent threshold
Scenario: Nightly workflow uses at least 96.5 percent threshold
Given the nightly quality workflow file is loaded for coverage check
Then the nightly workflow should use a fail-under of at least 97
Then the nightly workflow should use a fail-under of at least 96.5
+3 -3
View File
@@ -1,5 +1,5 @@
Feature: Coverage threshold configuration
The project enforces a minimum 97% code coverage threshold across multiple
The project enforces a minimum 96.5% code coverage threshold across multiple
configuration files. This feature validates that pyproject.toml, noxfile.py,
the CI workflow, and the nightly quality workflow are all correctly configured
to enforce and report coverage thresholds.
@@ -41,9 +41,9 @@ Feature: Coverage threshold configuration
Then the noxfile should define a coverage_report session
@tdd_issue @tdd_issue_4227
Scenario: Noxfile enforces fail-under threshold of at least 97
Scenario: Noxfile enforces fail-under threshold of at least 96.5
Given the noxfile py is loaded for coverage check
Then the noxfile should contain a fail-under threshold of at least 97
Then the noxfile should contain a fail-under threshold of at least 96.5
# -- CI workflow coverage enforcement --
@@ -4,10 +4,10 @@ Feature: Coverage threshold enforcement
are properly set up to enforce this requirement.
@tdd_issue @tdd_issue_4227
Scenario: Coverage threshold is configured at 97% in noxfile
Scenario: Coverage threshold is configured at 96.5% in noxfile
Given the noxfile.py exists
When I parse the COVERAGE_THRESHOLD constant from noxfile.py
Then the coverage threshold should be 97
Then the coverage threshold should be 96.5
Scenario: Coverage report session uses fail-under flag
Given the noxfile.py exists
-34
View File
@@ -29,40 +29,6 @@ Feature: Legacy plan persistence removal
When I instantiate ChangeRepository
Then a DeprecationWarning mentioning "ChangeRepository is deprecated" is raised
# --- CLI programmatic wrappers deprecation ---
Scenario: tell_command emits deprecation warning
When I call tell_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: build_command emits deprecation warning
When I call build_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: apply_command emits deprecation warning
When I call apply_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: new_command emits deprecation warning
When I call new_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: current_command emits deprecation warning
When I call current_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: list_command emits deprecation warning
When I call list_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: cd_command emits deprecation warning
When I call cd_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
Scenario: continue_command emits deprecation warning
When I call continue_command programmatically
Then a DeprecationWarning mentioning "legacy" is raised
# --- PlanLifecycleService is NOT deprecated ---
Scenario: PlanLifecycleService does NOT emit deprecation warnings
+27
View File
@@ -20,6 +20,33 @@ Feature: Plan CLI coverage boost
When I call _plan_spec_dict on the plan
Then the spec dict should not contain key "error_message"
Scenario: _plan_spec_dict includes estimation_result when set
Given a v3 Plan with estimation_result set
When I call _plan_spec_dict on the plan
Then the spec dict should contain key "estimation"
Scenario: _plan_spec_dict includes invariants when populated
Given a v3 Plan with invariants populated
When I call _plan_spec_dict on the plan
Then the spec dict should contain key "invariants"
Scenario: _plan_spec_dict includes execution_environment when set
Given a v3 Plan with execution_environment set
When I call _plan_spec_dict on the plan
Then the spec dict should contain key "execution_environment"
And the spec dict should contain key "execution_env_priority"
Scenario: _plan_spec_dict includes dod_evaluation when validation_summary set
Given a v3 Plan with validation_summary populated
When I call _plan_spec_dict on the plan
Then the spec dict should contain key "dod_evaluation"
Scenario: _plan_spec_dict includes last_completed_step and last_checkpoint_id
Given a v3 Plan with last_completed_step and last_checkpoint_id
When I call _plan_spec_dict on the plan
Then the spec dict should contain key "last_completed_step"
And the spec dict should contain key "last_checkpoint_id"
# ---- _print_lifecycle_plan helper ----
Scenario: _print_lifecycle_plan prints all optional timestamps
-22
View File
@@ -24,14 +24,6 @@ Feature: Plan CLI coverage round 3
When I plcov3 call _plan_spec_dict
Then the plcov3 spec dict should contain key "execution_environment" with value "container"
# ── build command PlanError handler (line 790) ──────────────────
Scenario: build command handles PlanError
Given a plcov3 CLI runner
And a plcov3 mocked build environment that raises PlanError
When I plcov3 invoke the build command
Then the plcov3 CLI output should contain "Build Error"
# ── apply command v3 path with plan_id (lines 799-824) ─────────
Scenario: apply command with plan_id delegates to v3 lifecycle
@@ -54,20 +46,6 @@ Feature: Plan CLI coverage round 3
When I plcov3 invoke apply without plan_id
Then the plcov3 CLI output should contain "No plans ready for apply"
# ── continue_plan command (lines 1205-1224) ─────────────────────
Scenario: continue command with prompt
Given a plcov3 CLI runner
And a plcov3 mocked continue environment
When I plcov3 invoke continue with prompt "add caching"
Then the plcov3 CLI output should contain "Added instructions"
Scenario: continue command without prompt shows current plan
Given a plcov3 CLI runner
And a plcov3 mocked continue environment with current plan
When I plcov3 invoke continue without prompt
Then the plcov3 CLI output should contain "Continuing with plan"
# ── use_action with strategy_actor (lines 1529-1542, 1631-1633, 1751-1752) ─
Scenario: use command with strategy-actor override
@@ -1,6 +1,6 @@
Feature: Plan V3 Lifecycle Commands and Streaming Coverage
Feature: Plan V3 Lifecycle Commands Coverage
As a developer
I want to test V3 lifecycle commands and streaming in plan.py
I want to test V3 lifecycle commands in plan.py
So that previously uncovered lines/branches have test coverage
# ===================================================================
@@ -8,59 +8,49 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
# ===================================================================
Scenario: Use action creates a plan from an available action with string arguments
Given I have a temporary test directory
When I invoke use_action with string argument "target_coverage=80"
Then the use_action CLI should succeed
And the lifecycle plan panel should be printed
Scenario: Use action parses integer and float argument values correctly
Given I have a temporary test directory
When I invoke use_action with args "count=42" and "ratio=3.14"
Then the use_action CLI should succeed
And the parsed arguments should contain integer 42 and float 3.14
Scenario: Use action parses boolean argument values correctly
Given I have a temporary test directory
When I invoke use_action with args "verbose=true" and "dry_run=false"
Then the use_action CLI should succeed
And the parsed arguments should contain booleans true and false
Scenario: Use action rejects invalid argument format without equals sign
Given I have a temporary test directory
When I invoke use_action with malformed argument "badarg"
Then the use_action CLI should abort
And the output should mention invalid argument format
Scenario: Use action falls back to action name lookup when ID not found
Given I have a temporary test directory
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 profile
Given I have a temporary test directory
When I invoke use_action with automation profile "full-auto"
Then the use_action CLI should succeed
And the lifecycle service should receive full-auto profile
Scenario: Use action rejects invalid automation profile
Given I have a temporary test directory
When I invoke use_action with automation profile "super_auto"
Then the use_action CLI should abort
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
When I invoke use_action and ActionNotAvailableError is raised
Then the use_action CLI should abort
And the output should mention action not available
Scenario: Use action handles validation errors gracefully
Given I have a temporary test directory
When I invoke use_action and ValidationError is raised
Then the use_action CLI should abort with lifecycle validation error
Scenario: Use action handles general CleverAgents errors
Given I have a temporary test directory
When I invoke use_action and CleverAgentsError is raised
Then the use_action CLI should abort with lifecycle general error
@@ -69,80 +59,62 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
# ===================================================================
Scenario: Execute plan transitions a strategize-complete plan to execute phase
Given I have a temporary test directory
When I invoke execute_plan with a valid plan ID
Then the execute_plan CLI should succeed
And the execute phase panel should be printed
Scenario: Execute plan auto-selects the only strategize-complete plan when no ID given
Given I have a temporary test directory
When I invoke execute_plan without ID and one strategize-complete plan exists
Then the execute_plan CLI should succeed
Scenario: Execute plan aborts when no plans are ready for execution
Given I have a temporary test directory
When I invoke execute_plan without ID and no strategize plans exist at all
Then the execute_plan CLI should abort
And the output should mention no plans ready for execution
Scenario: Execute plan aborts when multiple plans are ready and no ID given
Given I have a temporary test directory
When I invoke execute_plan without ID and multiple strategize-complete plans exist
Then the execute_plan CLI should abort
And the output should mention multiple plans ready
Scenario: Execute plan reports invalid phase transition error
Given I have a temporary test directory
When I invoke execute_plan and InvalidPhaseTransitionError is raised
Then the execute_plan CLI should abort
And the output should mention invalid transition
Scenario: Execute plan reports plan not ready error
Given I have a temporary test directory
When I invoke execute_plan and PlanNotReadyError is raised
Then the execute_plan CLI should abort
And the output should mention plan not ready
Scenario: Execute plan auto-runs strategize on queued plan
Given I have a temporary test directory
When I invoke execute_plan with a plan in strategize-queued state
Then the execute_plan CLI should succeed
And the PlanExecutor should have run strategize inline
Scenario: Execute plan succeeds when auto-progress already moved plan to execute
Given I have a temporary test directory
When I invoke execute_plan and auto-progress already moved plan to execute
Then the execute_plan CLI should succeed
Scenario: Plan use persists automation profile override
Given I have a temporary test directory
When I invoke use_action with automation profile "full-auto"
Then the use_action CLI should succeed
And the lifecycle service should persist the plan overrides
# ===================================================================
# V3 Lifecycle - apply_plan command
# ===================================================================
Scenario: Lifecycle apply transitions an execute-complete plan to apply phase
Given I have a temporary test directory
When I invoke apply with a valid plan ID
Then the apply CLI should succeed
And the apply phase panel should be printed
Scenario: Lifecycle apply auto-selects the only execute-complete plan
Given I have a temporary test directory
When I invoke apply without ID and one execute-complete plan exists
Then the apply CLI should succeed
Scenario: Lifecycle apply aborts when no plans are ready for apply
Given I have a temporary test directory
When I invoke apply without ID and no execute-complete plans exist
Then the apply CLI should abort
And the output should mention no plans ready for apply
Scenario: Lifecycle apply aborts when multiple plans are ready and no ID given
Given I have a temporary test directory
When I invoke apply without ID and multiple execute-complete plans exist
Then the apply CLI should abort
And the output should mention multiple plans ready for apply
@@ -152,19 +124,16 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
# ===================================================================
Scenario: Plan status shows details for a specific plan ID
Given I have a temporary test directory
When I invoke plan status with a specific plan ID
Then the plan status CLI should succeed
And the plan status panel should be displayed
Scenario: Plan status lists all active plans when no ID given
Given I have a temporary test directory
When I invoke plan status without ID and active plans exist
Then the plan status CLI should succeed
And the active plans table should be displayed
Scenario: Plan status shows no plans message when empty
Given I have a temporary test directory
When I invoke plan status without ID and no plans exist
Then the plan status CLI should succeed with no plans message
@@ -173,24 +142,20 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
# ===================================================================
Scenario: Lifecycle list shows all plans without filters
Given I have a temporary test directory
When I invoke list without filters
Then the list CLI should succeed
And the lifecycle plans table should be displayed
Scenario: Lifecycle list filters plans by phase
Given I have a temporary test directory
When I invoke list with phase filter "strategize"
Then the list CLI should succeed
Scenario: Lifecycle list rejects invalid phase filter
Given I have a temporary test directory
When I invoke list with phase filter "bogus_phase"
Then the list CLI should abort
And the output should mention invalid phase
Scenario: Lifecycle list shows no plans message when empty
Given I have a temporary test directory
When I invoke list and no plans match
Then the list CLI should succeed with empty list message
@@ -199,53 +164,32 @@ Feature: Plan V3 Lifecycle Commands and Streaming Coverage
# ===================================================================
Scenario: Cancel plan cancels a plan with a reason
Given I have a temporary test directory
When I invoke cancel with a reason "no longer needed"
Then the cancel CLI should succeed
And the output should show cancellation with reason
Scenario: Cancel plan cancels a plan without a reason
Given I have a temporary test directory
When I invoke cancel without a reason
Then the cancel CLI should succeed
And the output should show cancellation without reason text
Scenario: Cancel plan handles plan errors
Given I have a temporary test directory
When I invoke cancel and PlanError is raised
Then the cancel CLI should abort
And the output should mention cannot cancel
# ===================================================================
# Streaming (_tell_streaming)
# ===================================================================
Scenario: Tell streaming displays real-time progress for plan generation nodes
Given I have a temporary test directory
When I run _tell_streaming with a successful async event stream
Then _tell_streaming should complete without error
And the streaming output should include completion panel
Scenario: Tell streaming handles errors during streaming generation
Given I have a temporary test directory
When I run _tell_streaming with an error during streaming
Then _tell_streaming should raise the streaming error
# ===================================================================
# _print_lifecycle_plan
# ===================================================================
Scenario: Print lifecycle plan displays details for a real LifecyclePlan
Given I have a temporary test directory
When I call _print_lifecycle_plan with a LifecyclePlan instance
Then the lifecycle plan panel should be rendered with plan details
Scenario: Print lifecycle plan falls back for a non-LifecyclePlan object
Given I have a temporary test directory
When I call _print_lifecycle_plan with a non-LifecyclePlan object
Then the fallback plan panel should be rendered
Scenario: Print lifecycle plan shows error message when present
Given I have a temporary test directory
When I call _print_lifecycle_plan with a LifecyclePlan that has an error
Then the lifecycle plan panel should include the error message
-15
View File
@@ -124,18 +124,3 @@ Feature: ULID validation for v3 plan commands
When I invoke ulid-validation plan cancel with "my-legacy-plan"
Then the ulid-validation command should abort
And the ulid-validation output should contain "ULID"
# ===================================================================
# Legacy deprecation warning content
# ===================================================================
Scenario: Legacy tell command deprecation warning explains workflow incompatibility
When I call tell_command programmatically for ulid-validation
Then the ulid-validation deprecation warning should mention "incompatible"
And the ulid-validation deprecation warning should mention "agents plan use"
And the ulid-validation deprecation warning should not suggest simple command swap
Scenario: Legacy build command deprecation warning explains workflow incompatibility
When I call build_command programmatically for ulid-validation
Then the ulid-validation deprecation warning should mention "incompatible"
And the ulid-validation deprecation warning should mention "agents plan use"
+115
View File
@@ -105,6 +105,59 @@ def step_create_json_file_with_name(context, name):
context.import_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
# ---------------------------------------------------------------------------
# When — clear
# ---------------------------------------------------------------------------
@when('I run actor context clear "{name}" with --yes')
def step_clear_named_yes(context, name):
context.result = context.runner.invoke(
actor_context_app,
["clear", name, "--yes", "--context-dir", str(context.context_dir)],
)
@when("I run actor context clear --all with --yes")
def step_clear_all_yes(context):
context.result = context.runner.invoke(
actor_context_app,
["clear", "--all", "--yes", "--context-dir", str(context.context_dir)],
)
@when('I run actor context clear "{name}" with --yes and format "{fmt}"')
def step_clear_named_format(context, name, fmt):
context.result = context.runner.invoke(
actor_context_app,
[
"clear",
name,
"--yes",
"--context-dir",
str(context.context_dir),
"--format",
fmt,
],
)
@when("I run actor context clear without name or all")
def step_clear_no_args(context):
context.result = context.runner.invoke(
actor_context_app,
["clear", "--context-dir", str(context.context_dir)],
)
@when('I run actor context clear "{name}" with --all')
def step_clear_name_and_all(context, name):
context.result = context.runner.invoke(
actor_context_app,
["clear", name, "--all", "--context-dir", str(context.context_dir)],
)
# ---------------------------------------------------------------------------
# When — remove
# ---------------------------------------------------------------------------
@@ -330,6 +383,15 @@ def step_roundtrip_import(context, name):
# ---------------------------------------------------------------------------
@then("the actor context clear command should succeed")
def step_clear_success(context):
assert context.result.exit_code == 0, (
f"Expected exit 0, got {context.result.exit_code}.\n"
f"stdout: {context.result.output}\n"
f"stderr: {getattr(context.result, 'stderr', '')}"
)
@then("the actor context remove command should succeed")
def step_remove_success(context):
assert context.result.exit_code == 0, (
@@ -357,6 +419,15 @@ def step_import_success(context):
)
@then("the actor context clear command should fail with exit code 1")
def step_clear_fail(context):
assert context.result.exit_code == 1, (
f"Expected exit 1, got {context.result.exit_code}.\n"
f"stdout: {context.result.output}\n"
f"stderr: {getattr(context.result, 'stderr', '')}"
)
@then("the actor context remove command should fail with exit code 1")
def step_remove_fail(context):
assert context.result.exit_code == 1, (
@@ -409,6 +480,35 @@ def step_context_exists_check(context, name):
assert mgr.exists(), f"Context '{name}' does not exist at {mgr.context_dir}"
@then('the context "{name}" should be empty')
def step_context_empty(context, name):
mgr = ContextManager(name, context.context_dir)
assert len(mgr.messages) == 0, (
f"Context '{name}' still has messages: {mgr.messages}"
)
assert mgr.state == {}, f"Context '{name}' state not cleared: {mgr.state}"
assert mgr.global_context == {}, (
f"Context '{name}' global context not cleared: {mgr.global_context}"
)
@then("all actor contexts should be empty")
def step_all_contexts_empty(context):
for ctx_path in context.context_dir.iterdir():
if not ctx_path.is_dir():
continue
mgr = ContextManager(ctx_path.name, context.context_dir)
assert len(mgr.messages) == 0, (
f"Context '{ctx_path.name}' still has messages: {mgr.messages}"
)
assert mgr.state == {}, (
f"Context '{ctx_path.name}' state not cleared: {mgr.state}"
)
assert mgr.global_context == {}, (
f"Context '{ctx_path.name}' global context not cleared: {mgr.global_context}"
)
# ---------------------------------------------------------------------------
# Then — output assertions
# ---------------------------------------------------------------------------
@@ -422,6 +522,21 @@ def step_output_json_key(context, key):
assert key in data, f"Key '{key}' not found in JSON output: {data.keys()}"
@then('the output JSON key "{top_key}" should contain keys "{keys}"')
def step_output_json_nested_keys(context, top_key, keys):
parsed = json.loads(context.result.output)
data = _unwrap_envelope(parsed)
assert top_key in data, (
f"Key '{top_key}' not found in JSON output: {list(data.keys())}"
)
nested = data[top_key]
expected_keys = [k.strip() for k in keys.split(",")]
missing = [key for key in expected_keys if key and key not in nested]
assert not missing, (
f"Keys {missing} not found in nested output for '{top_key}': {list(nested.keys())}"
)
@then("the exported file should exist and contain valid JSON")
def step_exported_json_valid(context):
assert context.export_file.exists(), f"Export file {context.export_file} not found"
@@ -332,6 +332,18 @@ def step_assert_output_contains(context, text):
assert text in normalized, f"Expected to find '{text}' in CLI output: {output}"
@then('the output should not contain "{text}"')
def step_assert_output_not_contains(context, text):
import re
output = _capture_output(context)
raw = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", output)
normalized = " ".join(raw.split())
assert text not in normalized, (
f"Expected NOT to find '{text}' in CLI output: {output}"
)
@given(
"the build will fail {num_failures:d} times then succeed with {changes:d} changes"
)
-46
View File
@@ -14,11 +14,9 @@ from typer import Exit
from cleveragents.cli.main import (
_print_basic_help,
app,
build,
convert_exit_code,
ensure_cli_commands_registered,
main,
tell,
)
@@ -301,47 +299,3 @@ def step_check_registration_flag(context):
first_state, second_state = context.registration_states
assert first_state is True
assert second_state is True
@when("I invoke the tell shortcut with an actor override")
def step_call_tell_with_overrides(context):
"""Invoke tell shortcut ensuring actor override is forwarded."""
with patch("cleveragents.cli.commands.plan.tell") as mock_tell:
tell(
prompt="Explain coverage improvements",
name="coverage-plan",
actor="coverage-actor",
stream=True,
)
context.tell_kwargs = mock_tell.call_args.kwargs
@then("the tell shortcut should forward the actor override")
def step_check_tell_overrides(context):
"""Validate tell forwards actor override."""
expected = {
"prompt": "Explain coverage improvements",
"stream": True,
"name": "coverage-plan",
"actor": "coverage-actor",
}
assert context.tell_kwargs == expected, (
f"Expected {expected}, got {context.tell_kwargs}"
)
@when("I invoke the build shortcut with an actor override")
def step_call_build_with_overrides(context):
"""Invoke build shortcut ensuring actor override is forwarded."""
with patch("cleveragents.cli.commands.plan.build") as mock_build:
build(verbose=True, actor="build-actor")
context.build_kwargs = mock_build.call_args.kwargs
@then("the build shortcut should forward the actor override")
def step_check_build_overrides(context):
"""Validate build forwards actor override."""
expected = {"verbose": True, "actor": "build-actor"}
assert context.build_kwargs == expected, (
f"Expected {expected}, got {context.build_kwargs}"
)
@@ -81,24 +81,26 @@ def step_coverage_branch_enabled(context: Context) -> None:
raise AssertionError("branch = true not found in coverage config")
@then("the noxfile should contain a fail-under threshold of at least {threshold:d}")
def step_noxfile_fail_under(context: Context, threshold: int) -> None:
@then("the noxfile should contain a fail-under threshold of at least {threshold}")
def step_noxfile_fail_under(context: Context, threshold: str) -> None:
"""Assert noxfile has fail-under >= given threshold.
Supports both literal ``--fail-under=97`` and f-string
Supports both literal ``--fail-under=96.5`` and f-string
``f"--fail-under={COVERAGE_THRESHOLD}"`` patterns. When the
f-string form is found, the COVERAGE_THRESHOLD constant value
is resolved from the source.
"""
import ast
threshold_val = float(threshold)
# First try literal --fail-under=N
matches = re.findall(r"--fail-under=(\d+)", context.noxfile_text)
matches = re.findall(r"--fail-under=([\d.]+)", context.noxfile_text)
if matches:
max_threshold = max(int(m) for m in matches)
if max_threshold < threshold:
max_threshold = max(float(m) for m in matches)
if max_threshold < threshold_val:
raise AssertionError(
f"fail-under={max_threshold} is below required {threshold}"
f"fail-under={max_threshold} is below required {threshold_val}"
)
return
@@ -117,10 +119,10 @@ def step_noxfile_fail_under(context: Context, threshold: int) -> None:
and isinstance(node.value, ast.Constant)
):
raw = node.value.value
value = int(str(raw))
if value < threshold:
value = float(str(raw))
if value < threshold_val:
raise AssertionError(
f"COVERAGE_THRESHOLD={value} is below required {threshold}"
f"COVERAGE_THRESHOLD={value} is below required {threshold_val}"
)
return
raise AssertionError("No --fail-under found in noxfile.py")
@@ -172,19 +174,20 @@ def step_ci_coverage_nox_session(context: Context) -> None:
raise AssertionError("CI workflow does not reference 'nox -s coverage_report'")
@then("the nightly workflow should use a fail-under of at least {threshold:d}")
def step_nightly_fail_under(context: Context, threshold: int) -> None:
@then("the nightly workflow should use a fail-under of at least {threshold}")
def step_nightly_fail_under(context: Context, threshold: str) -> None:
"""Assert nightly workflow uses fail-under >= threshold."""
threshold_val = float(threshold)
# Check for --fail-under=N pattern (slipcover/coverage CLI)
matches = re.findall(r"--fail-under=(\d+)", context.nightly_workflow_text)
matches = re.findall(r"--fail-under=([\d.]+)", context.nightly_workflow_text)
# Also check for --coverage-min N pattern (quality gates script)
matches += re.findall(r"--coverage-min\s+(\d+)", context.nightly_workflow_text)
matches += re.findall(r"--coverage-min\s+([\d.]+)", context.nightly_workflow_text)
if not matches:
raise AssertionError(
"No --fail-under or --coverage-min found in nightly workflow"
)
max_threshold = max(int(m) for m in matches)
if max_threshold < threshold:
max_threshold = max(float(m) for m in matches)
if max_threshold < threshold_val:
raise AssertionError(
f"Nightly fail-under={max_threshold} is below required {threshold}"
f"Nightly fail-under={max_threshold} is below required {threshold_val}"
)
@@ -60,11 +60,19 @@ def step_parse_threshold(context: Any) -> None:
context.coverage_threshold = threshold
@then("the coverage threshold should be {expected:d}")
def step_check_threshold(context: Any, expected: int) -> None:
@then("the coverage threshold should be {expected}")
def step_check_threshold(context: Any, expected: str) -> None:
actual = context.coverage_threshold
if actual != expected:
raise AssertionError(f"Expected coverage threshold {expected}, got {actual}")
expected_val = float(expected)
if isinstance(actual, float):
if abs(actual - expected_val) > 0.0001:
raise AssertionError(
f"Expected coverage threshold {expected_val}, got {actual}"
)
elif actual != expected_val:
raise AssertionError(
f"Expected coverage threshold {expected_val}, got {actual}"
)
@when("I read the coverage_report session source from noxfile.py")
@@ -15,6 +15,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
from io import StringIO
from typing import Any
from unittest.mock import MagicMock, patch
from behave import given, then, when
@@ -22,8 +23,23 @@ from typer.testing import CliRunner
from cleveragents.cli.commands import plan as plan_module
from cleveragents.cli.commands.plan import (
_plan_spec_dict,
_print_lifecycle_plan,
_plan_spec_dict,
)
from cleveragents.cli.commands.plan import (
app as plan_app,
)
from cleveragents.domain.models.core.estimation import EstimationResult
from cleveragents.domain.models.core.plan import (
ExecutionEnvPriority,
NamespacedName,
Plan,
PlanIdentity,
PlanInvariant,
PlanPhase,
PlanTimestamps,
ProcessingState,
ProjectLink,
)
from cleveragents.cli.commands.plan import (
app as plan_app,
@@ -62,6 +78,13 @@ def _make_plan(
timestamps: PlanTimestamps | None = None,
project_links: list[ProjectLink] | None = None,
action_name: str = "local/test-action",
estimation_result: EstimationResult | None = None,
invariants: list[PlanInvariant] | None = None,
execution_environment: str | None = None,
execution_env_priority: ExecutionEnvPriority | None = None,
validation_summary: dict[str, Any] | None = None,
last_completed_step: int = -1,
last_checkpoint_id: str | None = None,
) -> Plan:
"""Build a real Plan object for testing."""
if timestamps is None:
@@ -83,6 +106,13 @@ def _make_plan(
timestamps=timestamps,
reusable=True,
read_only=False,
estimation_result=estimation_result,
invariants=invariants or [],
execution_environment=execution_environment,
execution_env_priority=execution_env_priority,
validation_summary=validation_summary,
last_completed_step=last_completed_step,
last_checkpoint_id=last_checkpoint_id,
)
@@ -143,6 +173,13 @@ def step_spec_dict_not_contains_key(context, key: str) -> None:
)
@then('the spec dict should contain key "{key}"')
def step_spec_dict_contains_key(context, key: str) -> None:
assert key in context.spec_dict, (
f"Key '{key}' not in spec dict: {context.spec_dict}"
)
# --------------------------------------------------------------------------
# _print_lifecycle_plan helpers
# --------------------------------------------------------------------------
@@ -389,6 +426,65 @@ def step_service_can_cancel(context) -> None:
context._cancel_plan_id = _ULIDS[2]
# ---- Additional _plan_spec_dict helpers ----
@given("a v3 Plan with estimation_result set")
def step_plan_with_estimation_result(context) -> None:
context.test_plan = _make_plan(
estimation_result=EstimationResult(
estimated_cost_usd=1.5,
estimated_tokens=5000,
risk_level="low",
summary="Low-risk change",
)
)
@given("a v3 Plan with invariants populated")
def step_plan_with_invariants(context) -> None:
context.test_plan = _make_plan(
invariants=[
PlanInvariant(
text="Do not modify production code",
source="action",
),
PlanInvariant(
text="Follow PEP 8 style",
source="action",
),
]
)
@given("a v3 Plan with execution_environment set")
def step_plan_with_execution_env(context) -> None:
context.test_plan = _make_plan(
execution_environment=".devcontainer/devcontainer.json",
execution_env_priority=ExecutionEnvPriority.OVERRIDE,
)
@given("a v3 Plan with validation_summary populated")
def step_plan_with_validation_summary(context) -> None:
context.test_plan = _make_plan(
validation_summary={
"dod_evaluated": True,
"dod_all_passed": True,
"required_passed": 3,
"required_failed": 0,
}
)
@given("a v3 Plan with last_completed_step and last_checkpoint_id")
def step_plan_with_step_and_checkpoint(context) -> None:
context.test_plan = _make_plan(
last_completed_step=5,
last_checkpoint_id="01HXYZ1234567890ABCDEFGHKJ",
)
# ---- When steps for CLI invocations ----
@@ -3,7 +3,6 @@
Targets remaining uncovered lines in cleveragents/cli/commands/plan.py:
- Lines 104, 107-108: validate_namespaced_actor invalid / valid
- Line 184: _plan_spec_dict with execution_environment
- Line 790: build command PlanError handler
- Lines 799-858: apply command (v3 and legacy paths)
- Lines 1205-1224: continue_plan command body
- Lines 1529-1542, 1631-1633, 1751-1752: use_action with actor overrides
@@ -31,7 +30,6 @@ from cleveragents.cli.commands.plan import (
)
from cleveragents.core.exceptions import (
CleverAgentsError,
PlanError,
ValidationError,
)
from cleveragents.domain.models.core.plan import (
@@ -178,31 +176,6 @@ def step_plcov3_spec_dict_contains(context: Context, key: str, value: str) -> No
)
# ══════════════════════════════════════════════════════════════
# build command PlanError handler (line 790)
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked build environment that raises PlanError")
def step_plcov3_mock_build_plan_error(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
plan_service.build_plan.side_effect = PlanError("Build failed internally")
container.plan_service.return_value = plan_service
container.actor_registry.return_value = MagicMock()
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@when("I plcov3 invoke the build command")
def step_plcov3_invoke_build(context: Context) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["build"])
# ══════════════════════════════════════════════════════════════
# apply command — v3 lifecycle path (lines 799-824)
# ══════════════════════════════════════════════════════════════
@@ -321,46 +294,6 @@ def step_plcov3_invoke_apply_no_id(context: Context) -> None:
# ══════════════════════════════════════════════════════════════
@given("a plcov3 mocked continue environment")
def step_plcov3_mock_continue(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
plan_service.continue_plan.return_value = None
container.plan_service.return_value = plan_service
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@given("a plcov3 mocked continue environment with current plan")
def step_plcov3_mock_continue_with_plan(context: Context) -> None:
container = MagicMock()
plan_service = MagicMock()
current_plan = MagicMock()
current_plan.name = "my-existing-plan"
plan_service.get_current_plan.return_value = current_plan
container.plan_service.return_value = plan_service
project = MagicMock()
project.name = "test-project"
_start_patch(context, _PATCH_CONTAINER, return_value=container)
_start_patch(context, _PATCH_GET_PROJECT, return_value=project)
@when('I plcov3 invoke continue with prompt "{prompt}"')
def step_plcov3_invoke_continue_with_prompt(context: Context, prompt: str) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["continue", prompt])
@when("I plcov3 invoke continue without prompt")
def step_plcov3_invoke_continue_no_prompt(context: Context) -> None:
context.plcov3_result = context.plcov3_runner.invoke(plan_app, ["continue"])
# ══════════════════════════════════════════════════════════════
# use_action with overrides (lines 1529-1542, 1631-1633, 1751-1752,
# 1789-1792, 1794)
@@ -2,34 +2,17 @@
from __future__ import annotations
import asyncio
import os
import re
from io import StringIO
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
from behave import given, then, when
from rich.console import Console
from behave import then, when
from typer.testing import CliRunner
from cleveragents.application.services.plan_service import PlanService
from cleveragents.cli.commands.plan import _tell_streaming
from cleveragents.cli.commands.plan import app as plan_app
class _StreamingPlanService:
def __init__(self, events: list[dict[str, object]], error: Exception | None = None):
self._events = events
self._error = error
async def generate_plan_streaming(self, *args, **kwargs):
for event in self._events:
yield event
if self._error:
raise self._error
class _PlanTellContainer:
def __init__(self, plan_service, project_service, actor_service):
self._plan_service = plan_service
@@ -46,76 +29,6 @@ class _PlanTellContainer:
return self._actor_service
def _strip_rich_markup(text: str) -> str:
return re.sub(r"\[[^\]]+\]", "", text)
def _run_streaming_helper(context, plan_service, description: str) -> None:
output = StringIO()
test_console = Console(file=output, force_terminal=False, width=120)
error_message = ""
success = False
try:
with patch("cleveragents.cli.commands.plan.console", test_console):
asyncio.run(
_tell_streaming(
context.streaming_project,
description,
None,
plan_service,
)
)
success = True
except Exception as exc:
error_message = str(exc)
finally:
rendered = _strip_rich_markup(output.getvalue())
context.command_output = rendered if success else f"{rendered}{error_message}"
context.command_success = success
context.command_error = error_message
@given("I have a stub streaming project")
def step_stub_streaming_project(context):
context.streaming_project = SimpleNamespace(name="streaming-project")
@when("I run the streaming plan helper with only an end event")
def step_run_streaming_end_event(context):
service = _StreamingPlanService(events=[{"__end__": True}])
_run_streaming_helper(context, service, "End-only streaming plan")
@when("I run the streaming plan helper with a pre-node exception")
def step_run_streaming_pre_node_exception(context):
service = _StreamingPlanService(events=[], error=RuntimeError("boom"))
_run_streaming_helper(context, service, "Exception streaming plan")
@then("the streaming helper should complete successfully")
def step_streaming_helper_success(context):
assert context.command_success, f"Streaming failed: {context.command_output}"
@then("the streaming output should mention plan creation success")
def step_streaming_output_mentions_success(context):
output = context.command_output
assert "Plan created and built" in output or "Plan generated successfully" in output
@then("the streaming helper should fail with an error")
def step_streaming_helper_failure(context):
assert not context.command_success
assert context.command_output or context.command_error
@then("the streaming output should show an error without node failure details")
def step_streaming_output_no_node_failure(context):
output = context.command_output.lower()
assert "error" in output
assert "failed" not in output
@when("I execute plan tell with testing mode disabled and no actor registry")
def step_plan_tell_testing_mode_disabled(context):
runner = CliRunner()
+6
View File
@@ -2406,6 +2406,12 @@ def step_check_changes_marked_applied(context: Context) -> None:
assert context.applied_count > 0
@then("the changes should be marked as applied")
def step_check_changes_applied(context: Context) -> None:
"""Check that changes were marked as applied."""
assert context.applied_count > 0
@given("the plan has a change that will fail on apply")
def step_add_failing_change(context: Context) -> None:
"""Add a change that will fail when applied."""
+135 -76
View File
@@ -1,91 +1,150 @@
"""Step definitions for TDD test: agents actor run returns no useful response.
"""Step definitions for TDD Bug #10861 - agents actor run returns nothing.
Issue: #10861 — agents actor run does not work
TDD Issue: #10862
This test captures the bug: when `agents actor run` is invoked with a built-in
LLM actor name resolved from the registry, the command returns no useful
response (either empty or an error).
The @tdd_expected_fail tag inverts the result so CI passes while the bug exists.
Once the fix is applied, the @tdd_expected_fail tag must be removed and the
test must pass normally.
These steps verify that resolve_config_files synthesises a v3 type: llm YAML
when the actor has a built-in config_blob with provider and model but no type
field. This is the regression guard for bug #10861.
"""
from __future__ import annotations
from typing import Any
import yaml
from pathlib import Path
from unittest.mock import MagicMock, patch
from behave import given, then, when
from typer.testing import CliRunner
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
from cleveragents.cli.commands.actor import app as actor_app
from cleveragents.cli.commands._resolve_actor import resolve_config_files
@given("I have a mock LLM that returns {response}")
def step_have_mock_llm(context: Any, response: str) -> None:
"""Create a mock LLM that returns the specified response."""
context.mock_llm_response = response
context.mock_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = response
context.mock_llm.invoke.return_value = mock_response
@given("a built-in actor with provider and model but no type field for tdd-10861")
def step_given_builtin_actor(context: Context) -> None:
"""Set up a mock built-in actor with provider/model but no type field."""
mock_actor = MagicMock()
mock_actor.name = "anthropic/claude-sonnet-4-20250514"
mock_actor.yaml_text = ""
mock_actor.config_blob = {
"provider": "anthropic",
"model": "claude-sonnet-4-20250514",
"capabilities": {},
"unsafe": False,
"source": "provider-registry",
}
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
context.mock_actor = mock_actor
context.mock_container = mock_container
@when("I run actor run with the built-in actor name and prompt {prompt}")
def step_run_actor_run_with_builtin(context: Any, prompt: str) -> None:
"""Invoke `agents actor run` with a built-in actor name and prompt."""
context.prompt = prompt
# After the virtual built-in refactor (issue #10923), built-in actors are
# no longer stored in actor_service.actors. Use the registry's virtual
# resolution to find the anthropic actor.
virtual_actors = context.registry._resolve_virtual_builtin_actors()
actor_name = next(
(a.name for a in virtual_actors if "anthropic" in a.name.lower()),
None,
@given("an actor with existing yaml_text for tdd-10861")
def step_given_actor_with_yaml_text(context: Context) -> None:
"""Set up a mock actor that already has yaml_text."""
mock_actor = MagicMock()
mock_actor.name = "local/my-custom-actor"
mock_actor.yaml_text = (
"name: local/my-custom-actor\ntype: llm\nprovider: openai\nmodel: gpt-4\n"
)
if not actor_name:
raise AssertionError("No built-in anthropic actor found")
context.actor_name = actor_name
context.runner = CliRunner()
def run_with_mocks():
# Build a mock container that returns the stub actor registry.
# This is needed because the CLI's resolve_config_files() calls
# get_container().actor_registry().get(name) — without this patch,
# it hits the real DI container which has no actors in CI.
mock_container = MagicMock()
mock_actor_registry = MagicMock()
# Resolve the virtual built-in actor from the registry.
mock_actor = context.registry.get_actor(actor_name)
mock_actor_registry.get.return_value = mock_actor
mock_container.actor_registry.return_value = mock_actor_registry
with (
patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=mock_container,
),
patch(
"cleveragents.providers.registry.ProviderRegistry.create_llm",
return_value=context.mock_llm,
),
):
return context.runner.invoke(
actor_app,
["run", actor_name, prompt],
)
context.result = run_with_mocks()
mock_actor.config_blob = None
mock_registry = MagicMock()
mock_registry.get.return_value = mock_actor
mock_container = MagicMock()
mock_container.actor_registry.return_value = mock_registry
context.mock_actor = mock_actor
context.mock_container = mock_container
@then("the actor run should return {expected}")
def step_actor_run_should_return(context: Any, expected: str) -> None:
"""Assert that the actor run command returned the expected response."""
output = context.result.output or ""
stderr = getattr(context.result, "stderr", "") or ""
combined = output + stderr
assert expected in combined, f"Expected '{expected}' in output: {combined!r}"
@when("resolve_config_files is called with the built-in actor name for tdd-10861")
def step_when_resolve_builtin(context: Context) -> None:
"""Call resolve_config_files with the built-in actor name."""
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=context.mock_container,
):
context.result_paths = resolve_config_files(
"anthropic/claude-sonnet-4-20250514", []
)
context.add_cleanup(
lambda: [p.unlink(missing_ok=True) for p in context.result_paths]
)
@when("resolve_config_files is called with the actor name for tdd-10861")
def step_when_resolve_actor(context: Context) -> None:
"""Call resolve_config_files with the actor name."""
with patch(
"cleveragents.cli.commands._resolve_actor.get_container",
return_value=context.mock_container,
):
context.result_paths = resolve_config_files("local/my-custom-actor", [])
context.add_cleanup(
lambda: [p.unlink(missing_ok=True) for p in context.result_paths]
)
@then("the resulting YAML file contains type llm for tdd-10861")
def step_then_yaml_contains_type_llm(context: Context) -> None:
"""Assert the synthesised YAML contains type: llm."""
assert len(context.result_paths) == 1
tmp_path: Path = context.result_paths[0]
assert tmp_path.exists(), f"Temp file does not exist: {tmp_path}"
content = tmp_path.read_text(encoding="utf-8")
parsed = yaml.safe_load(content)
assert isinstance(parsed, dict), f"Expected dict, got {type(parsed)}"
assert parsed.get("type") == "llm", (
"Expected type: llm in synthesised YAML, got: "
+ str(parsed.get("type"))
+ "\nFull content:\n"
+ content
)
@then("the resulting YAML file contains the provider and model for tdd-10861")
def step_then_yaml_contains_provider_model(context: Context) -> None:
"""Assert the synthesised YAML contains the correct provider and model."""
tmp_path: Path = context.result_paths[0]
content = tmp_path.read_text(encoding="utf-8")
parsed = yaml.safe_load(content)
assert parsed.get("provider") == "anthropic", (
"Expected provider: anthropic, got: " + str(parsed.get("provider"))
)
assert parsed.get("model") == "claude-sonnet-4-20250514", (
"Expected model: claude-sonnet-4-20250514, got: " + str(parsed.get("model"))
)
@then("the resulting YAML file is parseable as a v3 llm actor config for tdd-10861")
def step_then_yaml_parseable_as_v3(context: Context) -> None:
"""Assert the synthesised YAML is parseable by ReactiveConfigParser."""
from cleveragents.reactive.config_parser import ReactiveConfigParser
tmp_path: Path = context.result_paths[0]
parser = ReactiveConfigParser()
rc = parser.parse_files([tmp_path])
assert rc.agents, (
"ReactiveConfigParser produced no agents from synthesised YAML. "
"This means run_single_shot() would return empty string (bug #10861)."
)
assert rc.routes, (
"ReactiveConfigParser produced no routes from synthesised YAML. "
"This means run_single_shot() would return empty string (bug #10861)."
)
@then("the original yaml_text is used without modification for tdd-10861")
def step_then_original_yaml_used(context: Context) -> None:
"""Assert that actors with existing yaml_text are not affected by the fix."""
tmp_path: Path = context.result_paths[0]
assert tmp_path.exists(), f"Temp file does not exist: {tmp_path}"
content = tmp_path.read_text(encoding="utf-8")
assert "local/my-custom-actor" in content, (
"Expected original yaml_text content, got: " + content
)
assert "type: llm" in content, (
"Expected type: llm from original yaml_text, got: " + content
)
assert "provider: openai" in content, (
"Expected provider: openai from original yaml_text, got: " + content
)
@@ -12,11 +12,13 @@ feature file and this test will run normally as a regression guard.
from __future__ import annotations
import logging
from unittest import mock
import structlog
from behave import given, then, when # type: ignore[import-untyped]
from behave.runner import Context # type: ignore[import-untyped]
import cleveragents.infrastructure.events.reactive as reactive_module
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.reactive import ReactiveEventBus
from cleveragents.infrastructure.events.types import EventType
@@ -40,48 +42,16 @@ def step_given_bus_with_failing_handler(context: Context) -> None:
@when("I emit an event that triggers the failing handler")
def step_when_emit_event(context: Context) -> None:
"""Emit a PLAN_CREATED event and capture structlog output."""
# Use Python logging to capture structlog output
# Create a handler that captures log records
logger = logging.getLogger("cleveragents.infrastructure.events.reactive")
# Create a handler that captures logs
class StructlogCapturingHandler(logging.Handler):
def __init__(self):
super().__init__()
self.records = []
def emit(self, record):
self.records.append(record)
handler = StructlogCapturingHandler()
handler.setLevel(logging.DEBUG)
logger.addHandler(handler)
try:
# Emit the event
# The logger in reactive.py is created at module load time. To capture
# logs from it, we must patch it with a logger that collects output.
# We use structlog.testing.capture_logs() context manager combined with
# a mock.patch to replace the module-level logger during emit().
with (
structlog.testing.capture_logs() as captured,
mock.patch.object(reactive_module, "_logger", structlog.get_logger()),
):
context.bus.emit(DomainEvent(event_type=EventType.PLAN_CREATED))
# Convert logging records to structlog format
context.captured_logs = []
for record in handler.records:
# structlog with stdlib integration stores the dict in record.msg
log_entry = {}
# When structlog is configured with stdlib.ProcessorFormatter,
# the structured data is stored directly in record.msg as a dict
if isinstance(record.msg, dict):
log_entry = record.msg
else:
# Fallback: ensure we at least have log_level
log_entry = {"event": record.getMessage()}
# Ensure we have the log_level
if "log_level" not in log_entry:
log_entry["log_level"] = record.levelname.lower()
context.captured_logs.append(log_entry)
finally:
logger.removeHandler(handler)
context.captured_logs = captured
@then("the warning log should contain the exception message text")
+28 -10
View File
@@ -1,12 +1,30 @@
@tdd_issue @tdd_issue_10861
Feature: TDD Issue #10862 — agents actor run returns no useful response
As a user who invokes `agents actor run` with a built-in LLM actor
I want to receive the LLM's response
So that the command is useful
Feature: TDD Bug #10861 - agents actor run returns nothing for built-in LLM actors
Scenario: Actor run with built-in LLM actor returns the LLM response
Given a configured provider registry with anthropic/claude-sonnet-4-20250514
And the built-in actors have been ensured
And I have a mock LLM that returns "feep"
When I run actor run with the built-in actor name and prompt "ping"
Then the actor run should return "feep"
Bug #10861 reports that running agents actor run with a built-in LLM actor
returns nothing instead of a response from the LLM.
Root cause: built-in actors have a config_blob with provider and model
fields but no type field. When serialised to YAML and fed to
ReactiveConfigParser, the parser produces an empty ReactiveConfig with no
agents and no routes, causing run_single_shot() to return empty string.
Fix: resolve_config_files now synthesises a minimal v3 type: llm YAML
when the actor has no yaml_text and the config_blob has provider and model
but no type field.
Scenario: resolve_config_files synthesises v3 llm YAML for built-in actor
Given a built-in actor with provider and model but no type field for tdd-10861
When resolve_config_files is called with the built-in actor name for tdd-10861
Then the resulting YAML file contains type llm for tdd-10861
And the resulting YAML file contains the provider and model for tdd-10861
Scenario: synthesised YAML is parseable by ReactiveConfigParser for tdd-10861
Given a built-in actor with provider and model but no type field for tdd-10861
When resolve_config_files is called with the built-in actor name for tdd-10861
Then the resulting YAML file is parseable as a v3 llm actor config for tdd-10861
Scenario: actor with existing yaml_text is not affected by the fix for tdd-10861
Given an actor with existing yaml_text for tdd-10861
When resolve_config_files is called with the actor name for tdd-10861
Then the original yaml_text is used without modification for tdd-10861
+4 -2
View File
@@ -163,6 +163,8 @@ def typecheck(session: nox.Session):
def unit_tests(session: nox.Session):
"""Run BDD tests with Behave."""
session.install("-e", ".[tests]")
# Explicitly ensure a2a-sdk is installed for A2A SDK dependency tests
session.install("a2a-sdk>=0.3.0")
_install_behave_parallel(session)
# Build a pre-migrated template DB so each scenario can copy it
@@ -506,7 +508,7 @@ def e2e_tests(session: nox.Session):
)
COVERAGE_THRESHOLD = 97 # Temporarily lowered due to many @tdd_expected_fail tests
COVERAGE_THRESHOLD = 96.5 # Temporarily lowered due to many @tdd_expected_fail tests
# see issues #4183 and #4184
@@ -519,7 +521,7 @@ def coverage_report(session: nox.Session):
so a single slipcover invocation collects coverage for the entire
suite -- no per-worker files or merge step required.
Coverage threshold is enforced at >=97%.
Coverage threshold is enforced at >=96.5%.
On success, emits: COVERAGE OK: <pct>% (threshold: 97%)
On failure, emits: COVERAGE FAILED: <pct>% < 97% threshold
-117
View File
@@ -42,130 +42,13 @@ Test Context Commands With Actor
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} main.py
Test Plan Creation With Actor
[Documentation] Test plan creation using actor instead of provider/model
# Initialize project
Create Directory ${TEST_PROJECT_DIR}_plan
${result} = Run Process ${PYTHON} -m cleveragents init test-plan-project
... cwd=${TEST_PROJECT_DIR}_plan
Should Be Equal As Integers ${result.rc} 0
# Create plan with actor
Set Environment Variable CLEVERAGENTS_TESTING_USE_MOCK_AI true
Set Environment Variable CLEVERAGENTS_DEFAULT_ACTOR anthropic/claude-3
${result} = Run Process ${PYTHON} -m cleveragents tell Create a hello world function
... cwd=${TEST_PROJECT_DIR}_plan env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Log Tell stdout: ${result.stdout}
Log Tell stderr: ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} Plan
Test Actor-Based Workflow
[Documentation] Test complete workflow with actor configuration
# Initialize
${project_dir} = Set Variable ${TEST_PROJECT_DIR}_workflow
Create Directory ${project_dir}
${result} = Run Process ${PYTHON} -m cleveragents init workflow-project
... cwd=${project_dir}
Should Be Equal As Integers ${result.rc} 0
# Set up actor environment
Set Environment Variable CLEVERAGENTS_TESTING_USE_MOCK_AI true
Set Environment Variable CLEVERAGENTS_DEFAULT_ACTOR openai/gpt-4
# Add context
Create File ${project_dir}/test.py def hello():\n${SPACE*4}pass
${result} = Run Process ${PYTHON} -m cleveragents context-load test.py
... cwd=${project_dir}
Should Be Equal As Integers ${result.rc} 0
# Create plan
${result} = Run Process ${PYTHON} -m cleveragents tell Add docstring to hello function
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Should Be Equal As Integers ${result.rc} 0
# Build plan
${result} = Run Process ${PYTHON} -m cleveragents build
# Normal duration: ~10-15s. Timeout raised from 30s to 120s for pabot
# cold-start (16 parallel processes) + Alembic migration overhead.
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
# NOTE: Legacy 'apply' was removed. Verify v3 apply --help instead.
${result} = Run Process ${PYTHON} -m cleveragents apply --help
... cwd=${project_dir}
Should Be Equal As Integers ${result.rc} 0
Test Multiple Actors In Project
[Documentation] Test switching between actors in a project
${project_dir} = Set Variable ${TEST_PROJECT_DIR}_multi_actor
# Initialize
Create Directory ${project_dir}
${result} = Run Process ${PYTHON} -m cleveragents init multi-actor-project
... cwd=${project_dir}
Should Be Equal As Integers ${result.rc} 0
Set Environment Variable CLEVERAGENTS_TESTING_USE_MOCK_AI true
# Create plan with first actor
Set Environment Variable CLEVERAGENTS_DEFAULT_ACTOR openai/gpt-3.5-turbo
${result} = Run Process ${PYTHON} -m cleveragents tell Create function A --name plan1
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Should Be Equal As Integers ${result.rc} 0
# Create plan with second actor
Set Environment Variable CLEVERAGENTS_DEFAULT_ACTOR anthropic/claude-3
${result} = Run Process ${PYTHON} -m cleveragents tell Create function B --name plan2
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Should Be Equal As Integers ${result.rc} 0
# Verify plans were created (legacy plan commands are deprecated;
# the v3 'plan list' command lists lifecycle plans only)
Log Legacy plan creation verified via 'tell' commands above
Test Context Clear Command
[Documentation] Test clearing all contexts
${project_dir} = Set Variable ${TEST_PROJECT_DIR}_clear
# Initialize and add contexts
Create Directory ${project_dir}
${result} = Run Process ${PYTHON} -m cleveragents init clear-project
... cwd=${project_dir}
Should Be Equal As Integers ${result.rc} 0
Set Environment Variable CLEVERAGENTS_TESTING_USE_MOCK_AI true
# Create a plan first
${result} = Run Process ${PYTHON} -m cleveragents tell Test clearing contexts
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Should Be Equal As Integers ${result.rc} 0
Create File ${project_dir}/file1.py # test file 1
Create File ${project_dir}/file2.py # test file 2
${result} = Run Process ${PYTHON} -m cleveragents context-load file1.py file2.py
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Should Be Equal As Integers ${result.rc} 0
# Clear contexts
${result} = Run Process ${PYTHON} -m cleveragents context clear --yes
... cwd=${project_dir} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true
Log Clear stdout: ${result.stdout}
Log Clear stderr: ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
# Verify contexts are cleared
${result} = Run Process ${PYTHON} -m cleveragents context list
... cwd=${project_dir}
Should Be Equal As Integers ${result.rc} 0
Should Not Contain ${result.stdout} file1.py
Should Not Contain ${result.stdout} file2.py
*** Keywords ***
Setup Test Environment
-120
View File
@@ -52,119 +52,21 @@ Add Files To Context
[Teardown] Cleanup Test Directory
Create Plan With Tell
[Documentation] Test tell command
[Setup] Initialize Test Project
${result}= Run Process ${PYTHON} -m cleveragents tell Add error handling
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
# Verify plan was created with correct name derived from prompt
${result}= Run Process ${PYTHON} -m cleveragents plan current
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Contain ${result.stdout} add_error_handling
[Teardown] Cleanup Test Directory
Build Plan
[Documentation] Test build command
[Setup] Initialize Test Project With Plan
${result}= Run Process ${PYTHON} -m cleveragents build
... cwd=${TEST_DIR} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
[Teardown] Cleanup Test Directory
Apply Plan Changes
[Documentation] Test apply command (v3 lifecycle; legacy apply removed)
[Tags] legacy_removed
[Setup] Initialize Test Project With Built Plan
# NOTE: Legacy 'apply' was removed. The v3 'apply' command requires
# a lifecycle plan. Verify the help text is accessible.
${result}= Run Process ${PYTHON} -m cleveragents apply --help
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
[Teardown] Cleanup Test Directory
Create New Empty Plan
[Documentation] Test plan new command
[Setup] Initialize Test Project
${result}= Run Process ${PYTHON} -m cleveragents plan new feature-plan
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
# Verify new plan is current
${result}= Run Process ${PYTHON} -m cleveragents plan current
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Contain ${result.stdout} feature-plan
[Teardown] Cleanup Test Directory
Show Current Plan
[Documentation] Test plan current command
[Setup] Initialize Test Project
${result}= Run Process ${PYTHON} -m cleveragents plan current
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} main
[Teardown] Cleanup Test Directory
List All Plans
[Documentation] Test plan list command (v3 lifecycle)
[Tags] legacy_removed
[Setup] Initialize Test Project With Multiple Plans
# NOTE: Legacy 'plan list' was removed; v3 'plan list' lists lifecycle
# plans only. Verify the command runs without error.
${result}= Run Process ${PYTHON} -m cleveragents plan list
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
[Teardown] Cleanup Test Directory
Switch Between Plans
[Documentation] Test plan cd command
[Setup] Initialize Test Project With Multiple Plans
# Switch to feature-1
${result}= Run Process ${PYTHON} -m cleveragents plan cd feature-1
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
# Verify switch
${result}= Run Process ${PYTHON} -m cleveragents plan current
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Contain ${result.stdout} feature-1
[Teardown] Cleanup Test Directory
Continue Working On Plan
[Documentation] Test plan continue command
[Setup] Initialize Test Project With Plan
${result}= Run Process ${PYTHON} -m cleveragents plan continue Also add logging
... cwd=${TEST_DIR} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
[Teardown] Cleanup Test Directory
List Context Files
[Documentation] Test context list command
@@ -271,29 +173,7 @@ Initialize Test Project
... cwd=${TEST_DIR} timeout=300s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Initialize Test Project With Plan
[Documentation] Initialize project and create a plan
Initialize Test Project
${result}= Run Process ${PYTHON} -m cleveragents tell Test instruction
... cwd=${TEST_DIR} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Initialize Test Project With Built Plan
[Documentation] Initialize project with a built plan
Initialize Test Project With Plan
${result}= Run Process ${PYTHON} -m cleveragents build
... cwd=${TEST_DIR} env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Initialize Test Project With Multiple Plans
[Documentation] Initialize project with multiple plans
Initialize Test Project
${result}= Run Process ${PYTHON} -m cleveragents plan new feature-1
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
${result}= Run Process ${PYTHON} -m cleveragents plan new feature-2
... cwd=${TEST_DIR} timeout=120s on_timeout=kill
Should Be Equal As Integers ${result.rc} 0
Initialize Test Project With Context
[Documentation] Initialize project and add files to context
+2 -71
View File
@@ -23,8 +23,6 @@ Test CLI Help Shows All Commands
Should Contain ${result.stdout} context
Should Contain ${result.stdout} plan
Should Contain ${result.stdout} init
Should Contain ${result.stdout} tell
Should Contain ${result.stdout} build
Should Contain ${result.stdout} apply
Test Project Initialization
@@ -106,59 +104,16 @@ Test Context Clear
Should Be Equal As Numbers ${result.rc} 0
Should Contain ${result.stdout} Cleared all files from context
Test Plan Creation With Tell
[Documentation] Test creating a plan using tell command
Create Directory ${TEST_DIR}/project7
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project7 timeout=120s
Should Be Equal As Numbers ${init_result.rc} 0
${result} = Run Process ${PYTHON} -m cleveragents tell Add error handling to main function
... cwd=${TEST_DIR}/project7 timeout=120s
Should Be Equal As Numbers ${result.rc} 0
Should Contain ${result.stdout} Plan created
Should Contain ${result.stdout} error handling
Test Plan Build
[Documentation] Test building a plan
Create Directory ${TEST_DIR}/project8
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project8 timeout=120s
Should Be Equal As Numbers ${init_result.rc} 0
${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example code cwd=${TEST_DIR}/project8 timeout=120s
Should Be Equal As Numbers ${tell_result.rc} 0
${result} = Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project8
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s
Should Be Equal As Numbers ${result.rc} 0
Should Contain ${result.stdout} Plan built successfully
Should Contain ${result.stdout} Generated
Should Contain ${result.stdout} change(s)
Test Plan Apply
[Documentation] Test applying plan changes
[Documentation] Test v3 apply command help
Create Directory ${TEST_DIR}/project9
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project9 timeout=120s
Should Be Equal As Numbers ${init_result.rc} 0
${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example file cwd=${TEST_DIR}/project9 timeout=120s
Should Be Equal As Numbers ${tell_result.rc} 0
${build_result}= Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project9
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s
Should Be Equal As Numbers ${build_result.rc} 0
# NOTE: Legacy 'apply' was removed. The v3 'apply' requires a lifecycle plan.
# The v3 'apply' requires a lifecycle plan.
# Verify the command help is accessible.
${result} = Run Process ${PYTHON} -m cleveragents apply --help cwd=${TEST_DIR}/project9 timeout=120s
Should Be Equal As Numbers ${result.rc} 0
Test Plan List
[Documentation] Test listing plans
Create Directory ${TEST_DIR}/project10
${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project10 timeout=120s
Should Be Equal As Numbers ${init_result.rc} 0
${tell_result}= Run Process ${PYTHON} -m cleveragents tell First plan cwd=${TEST_DIR}/project10 timeout=120s
Should Be Equal As Numbers ${tell_result.rc} 0
${plan_result}= Run Process ${PYTHON} -m cleveragents plan new second-plan cwd=${TEST_DIR}/project10 timeout=120s
Should Be Equal As Numbers ${plan_result.rc} 0
# NOTE: Legacy 'plan list' was removed; v3 'plan list' lists lifecycle plans only.
${result} = Run Process ${PYTHON} -m cleveragents plan list cwd=${TEST_DIR}/project10 timeout=120s
Should Be Equal As Numbers ${result.rc} 0
Test Shortcut Commands Work
[Documentation] Test that shortcut commands work properly
Create Directory ${TEST_DIR}/project11
@@ -171,30 +126,6 @@ Test Shortcut Commands Work
Should Be Equal As Numbers ${result.rc} 0
Should Contain ${result.stdout} Added 1 file(s) to context
Test End To End Workflow
[Documentation] Test complete workflow from init to apply
Create Directory ${TEST_DIR}/project12
# Initialize project
${init} = Run Process ${PYTHON} -m cleveragents init workflow-project
... cwd=${TEST_DIR}/project12 timeout=120s
Should Be Equal As Numbers ${init.rc} 0
# Add context
Create File ${TEST_DIR}/project12/input.py def main(): pass
${add} = Run Process ${PYTHON} -m cleveragents context add input.py
... cwd=${TEST_DIR}/project12 timeout=120s
Should Be Equal As Numbers ${add.rc} 0
# Create plan
${tell} = Run Process ${PYTHON} -m cleveragents tell Add logging to main function
... cwd=${TEST_DIR}/project12 timeout=120s
Should Be Equal As Numbers ${tell.rc} 0
# Build plan
${build} = Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project12
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s
Should Be Equal As Numbers ${build.rc} 0
# NOTE: Legacy 'apply' was removed. Verify v3 apply --help instead.
${apply} = Run Process ${PYTHON} -m cleveragents apply --help cwd=${TEST_DIR}/project12 timeout=120s
Should Be Equal As Numbers ${apply.rc} 0
Test Command Error Handling
[Documentation] Test error handling for invalid commands
${result} = Run Process ${PYTHON} -m cleveragents invalid-command timeout=120s
+5 -5
View File
@@ -1,18 +1,18 @@
*** Settings ***
Documentation Coverage threshold enforcement tests
... Validates that coverage configuration and nox session are
... properly set up to enforce the 97% coverage requirement.
... properly set up to enforce the 96.5% coverage requirement.
Resource ${CURDIR}/common.resource
Suite Setup Setup Test Environment
Suite Teardown Cleanup Test Environment
*** Test Cases ***
Noxfile Contains Coverage Threshold Constant
[Documentation] Verify COVERAGE_THRESHOLD = 97 is defined in noxfile.py
[Documentation] Verify COVERAGE_THRESHOLD = 96.5 is defined in noxfile.py
[Tags] coverage config
${content}= Get File ${WORKSPACE}/noxfile.py
Should Contain ${content} COVERAGE_THRESHOLD = 97
Should Contain ${content} COVERAGE_THRESHOLD = 96.5
Pyproject Contains Coverage Run Section
[Documentation] Verify [tool.coverage.run] section exists in pyproject.toml
@@ -32,8 +32,8 @@ Pyproject Coverage Source Includes Src
${content}= Get File ${WORKSPACE}/pyproject.toml
Should Contain ${content} source = ["src"
Coverage Threshold Is 97 In Noxfile
[Documentation] Verify noxfile enforces 97% threshold via fail-under
Coverage Threshold Is 96.5 In Noxfile
[Documentation] Verify noxfile enforces 96.5% threshold via fail-under
[Tags] coverage config tdd_issue tdd_issue_4227
${content}= Get File ${WORKSPACE}/noxfile.py
Should Contain ${content} --fail-under=
+39 -1
View File
@@ -92,6 +92,35 @@ Write Action Config
Create File ${yaml_path} ${config}\n
RETURN ${yaml_path}
Clean Workspace Template Files
[Documentation] Remove template files from the workspace and monorepo
... that the LLM may regenerate during formatting, to prevent
... add/add merge conflicts during plan apply.
...
... CleverAgents copies workspace template files into project
... directories during ``project create``, so this keyword
... must run **after** project registration and **before**
... plan launch. It also runs ``git clean -fd`` in the
... monorepo to remove any untracked files that were copied.
[Arguments] ${target_dir}=${SUITE_HOME}
@{conflicting}= Create List
... .flake8
... .pre-commit-config.yaml
... pyproject.toml
... requirements-dev.txt
... requirements.txt
... setup.py
... setup.cfg
... tox.ini
FOR ${file} IN @{conflicting}
${path}= Set Variable ${target_dir}${/}${file}
Run Keyword And Ignore Error Remove File ${path}
END
# Also run git clean to remove any untracked files/directories that
# the workspace template may have deposited in the monorepo.
${git_clean}= Run Process git clean -fd cwd=${target_dir} timeout=60s on_timeout=kill
Log git clean in ${target_dir}: rc=${git_clean.rc} level=DEBUG
Write Broken Action Config
[Documentation] Write a deliberately broken action YAML that uses a non-existent
... LLM actor. Plans created with this action will fail during
@@ -226,7 +255,8 @@ Apply Batch Plans
... timeout=${PLAN_TIMEOUT} expected_rc=None
Log Apply ${plan_id} rc=${apply.rc}: ${apply.stdout}
IF ${apply.rc} != 0
Log Plan ${plan_id} failed during apply (rc=${apply.rc}): ${apply.stderr} WARN
Log Plan ${plan_id} failed during apply (rc=${apply.rc}) stdout: ${apply.stdout} WARN
Log Plan ${plan_id} failed during apply (rc=${apply.rc}) stderr: ${apply.stderr} WARN
CONTINUE
END
Append To List ${applied_ids} ${plan_id}
@@ -253,6 +283,10 @@ Workflow 10 Full-Auto Batch Formatting
[Teardown] Run CleverAgents Command config set core.automation-profile manual expected_rc=None
Skip If No LLM Keys
# Prevent add/add merge conflicts during plan apply by removing workspace
# template files that the LLM may regenerate.
Clean Workspace Template Files
# --- Step 1: Create temp monorepo with badly-formatted packages ---
${monorepo} ${branch}= Create Temp Monorepo
Log Monorepo created at: ${monorepo} (branch: ${branch})
@@ -279,6 +313,10 @@ Workflow 10 Full-Auto Batch Formatting
# --- Step 3: Register resources and projects for all packages ---
Register Package Resources And Projects ${monorepo} ${branch} @{PACKAGE_NAMES}
# Clean workspace template files that were copied into the monorepo
# during project creation, to prevent add/add merge conflicts during apply.
Clean Workspace Template Files ${monorepo}
# --- Step 4: Create plans — healthy + broken ---
# 4a: Launch plans for healthy packages with the good action
@{plan_ids}= Launch Batch Plans @{PACKAGE_NAMES}
+1 -9
View File
@@ -17,15 +17,7 @@ Provider Registry Uses Env Defaults
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} registry-openai-ok
CLI Build Uses Actor Selection
[Documentation] Validate CLI --actor flag propagates to PlanService via DI
${result}= Run Process ${PYTHON} robot/helper_provider_registry.py cli-override
... cwd=${SRC_DIR}
... env:OPENROUTER_API_KEY=robot-openrouter-key
... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=
Log Process Failure ${result}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} cli-actor-selection-ok
*** Keywords ***
Log Process Failure
+1 -1
View File
@@ -22,7 +22,7 @@ import sys
from pathlib import Path
def check_coverage(min_coverage: int) -> tuple[bool, str]:
def check_coverage(min_coverage: float) -> tuple[bool, str]:
"""Check test coverage meets minimum threshold."""
result = subprocess.run(
["coverage", "report", "--format=total"],
+23 -4
View File
@@ -36,11 +36,26 @@ def create_template(output_path: str = "build/.template-migrated.db") -> None:
out = Path(output_path)
out.parent.mkdir(parents=True, exist_ok=True)
# Remove existing template so we always create fresh
if out.exists():
out.unlink()
# Remove existing template and any SQLite journal/WAL files so we always
# create fresh. SQLite may leave behind -wal/-shm files that prevent
# clean recreation.
for suffix in ("", "-wal", "-shm", "-journal"):
candidate = out.with_name(out.name + suffix)
if candidate.exists():
candidate.unlink(missing_ok=True)
db_url = f"sqlite:///{out.resolve()}"
# Use a temporary file path to avoid race conditions with parallel
# workers that may still be reading the old template. We create the
# database at a unique temp path, then atomically rename it into place.
import tempfile
fd, tmp_path = tempfile.mkstemp(
dir=out.parent, prefix=f".{out.name}.tmp.", suffix=".db"
)
os.close(fd)
tmp = Path(tmp_path)
db_url = f"sqlite:///{tmp.resolve()}"
engine = create_engine(db_url, connect_args={"check_same_thread": False})
# Create all 33 tables in one fast DDL batch
@@ -71,6 +86,10 @@ def create_template(output_path: str = "build/.template-migrated.db") -> None:
engine.dispose()
# Atomically move the temp database into place. This ensures parallel
# workers never see a partially-written template.
tmp.replace(out)
# Ensure the template database is writable (0o664 = rw-rw-r--)
# This prevents sqlite3.OperationalError: attempt to write a readonly database
# when tests copy and modify the template during test setup.
@@ -19,6 +19,7 @@ from __future__ import annotations
import atexit
import tempfile
from pathlib import Path
from typing import Any
import typer
import yaml
@@ -52,6 +53,42 @@ def _sanitize_name(name: str) -> str:
return "".join(c for c in name if c.isprintable())
def _synthesize_llm_yaml(actor_name: str, config_blob: dict[str, Any]) -> str:
"""Synthesize a v3 type: llm YAML from a built-in actor config blob.
Built-in actors generated from the provider registry have a config_blob
with provider and model fields but no type field. When this raw blob is
serialised to YAML and fed to ReactiveConfigParser, the parser finds no
type, no agents/actors map, and no routes key so it produces an empty
ReactiveConfig with no agents and no routes. run_single_shot() then
returns empty string because there is nothing to execute (fix #10861).
Args:
actor_name: The namespaced actor name.
config_blob: The actor canonical configuration blob from the registry.
Returns:
A YAML string in v3 type: llm format that the reactive config
parser can consume to produce a working single-agent graph route.
"""
provider = str(config_blob.get("provider") or "")
model = str(config_blob.get("model") or "")
v3_blob: dict[str, Any] = {
"name": actor_name,
"type": "llm",
"description": f"Built-in LLM actor for {provider}/{model}",
"provider": provider,
"model": model,
}
system_prompt = config_blob.get("system_prompt")
if system_prompt:
v3_blob["system_prompt"] = system_prompt
return yaml.safe_dump(v3_blob, default_flow_style=False, sort_keys=False)
def resolve_config_files(name: str, config: list[Path]) -> list[Path]:
"""Return config file paths, resolving *name* from the actor registry
when *config* is empty.
@@ -102,15 +139,28 @@ def resolve_config_files(name: str, config: list[Path]) -> list[Path]:
err=True,
)
raise typer.Exit(code=2)
try:
yaml_text = yaml.safe_dump(config_blob, default_flow_style=False)
except yaml.YAMLError:
typer.echo(
f"Error: Actor '{safe_name}' config_blob could not be "
"serialised to YAML.",
err=True,
)
raise typer.Exit(code=2) from None
# Fix #10861: built-in actors have a config_blob with provider
# and model but no type field. Serialising this blob as-is
# produces YAML that the reactive config parser cannot interpret
# (no agents, no routes -> empty ReactiveConfig -> empty response).
# Synthesise a v3 type: llm YAML so the parser can create a
# working agent and graph route.
if (
config_blob.get("provider")
and config_blob.get("model")
and not config_blob.get("type")
):
yaml_text = _synthesize_llm_yaml(actor.name, config_blob)
else:
try:
yaml_text = yaml.safe_dump(config_blob, default_flow_style=False)
except yaml.YAMLError:
typer.echo(
f"Error: Actor '{safe_name}' config_blob could not be "
"serialised to YAML.",
err=True,
)
raise typer.Exit(code=2) from None
with tempfile.NamedTemporaryFile(
delete=False, suffix=".yaml", mode="w", encoding="utf-8"
+168 -11
View File
@@ -1,16 +1,18 @@
"""Actor-scoped context management commands.
Implements ``agents actor context remove``, ``agents actor context export``,
and ``agents actor context import`` per the v3 specification. These commands
manage named conversation contexts stored under ``~/.cleveragents/context/``
using the :class:`~cleveragents.reactive.context_manager.ContextManager`
persistence layer.
Implements ``agents actor context clear``, ``agents actor context remove``,
``agents actor context export``, and ``agents actor context import`` per the
v3 specification. These commands manage named conversation contexts stored
under ``~/.cleveragents/context/`` using the
:class:`~cleveragents.reactive.context_manager.ContextManager` persistence
layer.
"""
from __future__ import annotations
import hashlib
import json
import shutil
from pathlib import Path
from typing import Annotated, Any
@@ -58,6 +60,14 @@ def _context_size_kb(ctx_mgr: ContextManager) -> float:
return round(total / 1024, 1)
def _format_storage_value(kilobytes: float) -> str:
"""Return a human-readable storage string like ``"48 KB freed"``."""
rounded = round(kilobytes, 1)
if rounded.is_integer():
return f"{int(rounded)} KB freed"
return f"{rounded:.1f} KB freed"
def _file_checksum(path: Path) -> str:
"""Return ``sha256:<hex>`` checksum for a file."""
h = hashlib.sha256()
@@ -133,8 +143,6 @@ def context_remove(
agents actor context remove docs
agents actor context remove --all --yes
"""
import shutil
if name and all_contexts:
typer.echo("Error: Cannot specify NAME when using --all", err=True)
raise typer.Exit(code=1)
@@ -231,6 +239,147 @@ def context_remove(
_render_output(data, fmt, rich_panels=panels, ok_message="Context updated")
@app.command("clear")
def context_clear(
name: Annotated[
str | None,
typer.Argument(help="Context name to clear"),
] = None,
all_contexts: Annotated[
bool,
typer.Option("--all", "-a", help="Clear all contexts"),
] = False,
yes: Annotated[
bool,
typer.Option("--yes", "-y", help="Skip confirmation prompt"),
] = False,
context_dir: Annotated[
Path | None,
typer.Option(
"--context-dir",
help="Directory where contexts are stored",
resolve_path=True,
),
] = None,
fmt: Annotated[
str,
typer.Option("--format", "-f", help=_FORMAT_HELP),
] = "rich",
) -> None:
"""Clear message history for a named actor context or all contexts.
The context directory is preserved while messages, metadata, and state are
reset to their initial empty values.
Examples::
agents actor context clear docs
agents actor context clear --all --yes
"""
if name and all_contexts:
typer.echo("Error: Cannot specify NAME when using --all", err=True)
raise typer.Exit(code=1)
if not name and not all_contexts:
typer.echo("Error: Must specify NAME or use --all", err=True)
raise typer.Exit(code=1)
base = _default_context_base(context_dir)
contexts: list[str]
if all_contexts:
contexts = _list_context_names(base)
if not contexts:
typer.echo("No contexts found to clear.")
return
if not yes:
typer.echo(f"Found {len(contexts)} context(s) to clear:")
for cname in contexts:
typer.echo(f" - {cname}")
if not typer.confirm("Clear all?"):
typer.echo("Clear cancelled.")
return
else:
assert name is not None
target = base / name
if not target.exists():
typer.echo(f"Error: Context '{name}' does not exist.", err=True)
raise typer.Exit(code=1)
if not yes and not typer.confirm(f"Clear context '{name}'?"):
typer.echo("Clear cancelled.")
return
contexts = [name]
cleared_stats: dict[str, tuple[int, float]] = {}
total_messages_removed = 0
total_storage_freed = 0.0
for cname in contexts:
manager = ContextManager(cname, context_dir)
messages_before = len(manager.messages)
size_before = _context_size_kb(manager)
manager.clear()
size_after = _context_size_kb(manager)
freed = max(size_before - size_after, 0.0)
cleared_stats[cname] = (messages_before, freed)
total_messages_removed += messages_before
total_storage_freed += freed
cleared_context_count = len(contexts)
context_label = "all" if all_contexts else contexts[0]
if all_contexts:
items_value = f"{total_messages_removed} removed"
storage_value = _format_storage_value(total_storage_freed)
else:
messages_removed, storage_freed = cleared_stats[context_label]
items_value = f"{messages_removed} removed"
storage_value = _format_storage_value(storage_freed)
data: dict[str, Any] = {
"context_cleared": {
"context": context_label,
"items": items_value,
"storage": storage_value,
},
"retention": {
"context": "preserved",
"files": "cleared",
},
}
if all_contexts:
context_body = (
f"[bold]Context:[/bold] all ({cleared_context_count} cleared)\n"
f"[bold]Items:[/bold] {items_value}\n"
f"[bold]Storage:[/bold] {storage_value}"
)
else:
context_body = (
f"[bold]Context:[/bold] {context_label}\n"
f"[bold]Items:[/bold] {items_value}\n"
f"[bold]Storage:[/bold] {storage_value}"
)
panels = [
(
"Context Cleared",
context_body,
),
(
"Retention",
"[bold]Context:[/bold] preserved\n[bold]Files:[/bold] cleared",
),
]
_render_output(data, fmt, rich_panels=panels, ok_message="Context cleared")
@app.command("export")
def context_export(
name: Annotated[
@@ -238,14 +387,14 @@ def context_export(
typer.Argument(help="Context name to export"),
],
output: Annotated[
Path,
Path | None,
typer.Option(
"--output",
"-o",
help="Output file path (JSON or YAML)",
resolve_path=True,
),
] = ..., # type: ignore[assignment]
] = None,
context_dir: Annotated[
Path | None,
typer.Option(
@@ -270,6 +419,10 @@ def context_export(
agents actor context export docs --output /tmp/docs-context.json
agents actor context export docs -o ctx.yaml --format json
"""
if output is None:
typer.echo("Error: --output is required.", err=True)
raise typer.Exit(code=1)
base = _default_context_base(context_dir)
if not (base / name).exists():
typer.echo(f"Error: Context '{name}' does not exist.", err=True)
@@ -339,7 +492,7 @@ def context_import(
),
] = None,
input_file: Annotated[
Path,
Path | None,
typer.Option(
"--input",
"-i",
@@ -350,7 +503,7 @@ def context_import(
readable=True,
resolve_path=True,
),
] = ..., # type: ignore[assignment]
] = None,
update: Annotated[
bool,
typer.Option("--update", help="Replace existing context with same name"),
@@ -379,6 +532,10 @@ def context_import(
agents actor context import docs --input /tmp/docs-context.json
agents actor context import --input ctx.yaml --update
"""
if input_file is None:
typer.echo("Error: --input is required.", err=True)
raise typer.Exit(code=1)
# Parse input file (JSON or YAML)
text = input_file.read_text(encoding="utf-8")
suffix = input_file.suffix.lower()
+3 -838
View File
@@ -26,7 +26,6 @@ import os
import re
import shutil
import time
import warnings
from contextlib import suppress
from datetime import datetime
from pathlib import Path
@@ -37,7 +36,6 @@ import typer
from rich.console import Console
from rich.markup import escape as rich_escape
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table
from sqlalchemy.exc import SQLAlchemyError
@@ -204,19 +202,6 @@ def _validate_plan_ulid(plan_id: str) -> str:
return plan_id
_LEGACY_DEPRECATION_MSG = (
"This command uses the legacy plan workflow and is deprecated.\n"
"WARNING: The legacy and v3 plan workflows are INCOMPATIBLE and cannot\n"
"be mixed. Plans created with legacy commands ('agents tell', 'agents build')\n"
"exist only in the legacy storage system and cannot be referenced by v3\n"
"commands ('agents plan execute', 'agents plan apply').\n\n"
"To migrate to the v3 workflow:\n"
" 1. Use 'agents plan use <action> <project>' to create a new v3 plan.\n"
" 2. Use 'agents plan execute <PLAN_ID>' to execute it.\n"
" 3. Use 'agents plan apply <PLAN_ID>' to apply changes.\n\n"
"Do NOT attempt to use a legacy plan name with v3 commands — it will fail."
)
if TYPE_CHECKING:
from cleveragents.application.services.plan_apply_service import (
PlanApplyService,
@@ -224,13 +209,14 @@ if TYPE_CHECKING:
from cleveragents.application.services.plan_lifecycle_service import (
PlanLifecycleService,
)
from cleveragents.domain.models.core import Change, Plan, Project
from cleveragents.domain.models.core import Project
from cleveragents.domain.models.core.decision import Decision
# Create sub-app for plan commands
app = typer.Typer(
help=(
"Plan management commands (actor required; set default via "
"V3 Plan Lifecycle: Create plans with 'use', execute with 'execute', "
"apply changes with 'apply'. (Actor required; set default via "
"'agents actor set-default')"
)
)
@@ -491,246 +477,6 @@ def _execute_output_dict(
}
# Programmatic wrapper functions for testing and scripting
def tell_command(prompt: str, name: str | None = None) -> None:
"""Programmatic interface for creating a plan from instructions.
.. deprecated::
Use ``PlanLifecycleService.use_action`` instead.
Args:
prompt: Instructions for what you want the AI to do
name: Optional name for the plan
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Create the plan
plan_service.create_plan(project=project, prompt=prompt, name=name)
def build_command(
verbose: bool = False,
actor: str | None = None,
) -> list[Change]:
"""Programmatic interface for building the current plan.
.. deprecated::
Use ``PlanLifecycleService`` execute phase instead.
Args:
verbose: Whether to show detailed output
actor: Optional actor name override
Returns:
List of generated changes
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Build the plan
changes = plan_service.build_plan(
project=project,
actor=actor,
)
return changes if changes else []
def apply_command(confirm: bool = True) -> int:
"""Programmatic interface for applying plan changes.
.. deprecated::
Use ``PlanLifecycleService`` apply phase instead.
Args:
confirm: Whether to skip confirmation (for testing)
Returns:
Number of changes applied
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Apply changes
return plan_service.apply_changes(project=project)
def new_command(name: str) -> None:
"""Programmatic interface for creating a new empty plan.
.. deprecated::
Use ``PlanLifecycleService.use_action`` instead.
Args:
name: Name for the new plan
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Create new plan
plan_service.new_plan(project=project, name=name)
def current_command() -> Plan | None:
"""Programmatic interface for getting the current plan.
.. deprecated::
Use ``PlanLifecycleService.get_plan`` or ``list_plans`` instead.
Returns:
Current plan or None if no current plan
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Get current plan
return plan_service.get_current_plan(project=project)
def list_command() -> list[Plan]:
"""Programmatic interface for listing all plans.
.. deprecated::
Use ``PlanLifecycleService.list_plans`` instead.
Returns:
List of all plans in the current project
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Get all plans
plans = plan_service.list_plans(project=project)
return plans if plans else []
def cd_command(name: str) -> None:
"""Programmatic interface for switching to a different plan.
.. deprecated::
Use ``PlanLifecycleService.get_plan`` instead.
Args:
name: Name of the plan to switch to
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Switch to plan
plan_service.switch_to_plan(project=project, name=name)
def continue_command(prompt: str | None = None) -> None:
"""Programmatic interface for continuing work on the current plan.
.. deprecated::
Use ``PlanLifecycleService`` phase methods instead.
Args:
prompt: Optional additional instructions
"""
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
from cleveragents.application.services.project_service import ProjectService
container = get_container()
plan_service: PlanService = container.plan_service()
project_service: ProjectService = container.project_service()
# Get current project
project = project_service.get_current_project()
if not project:
raise CleverAgentsError("No project found. Run 'agents init' first.")
# Continue the plan
if prompt:
plan_service.continue_plan(project=project, prompt=prompt)
else:
# Just verify there's a current plan
plan = plan_service.get_current_plan(project=project)
if not plan:
raise CleverAgentsError("No current plan to continue.")
def _get_current_project() -> Project:
"""Get the current project or exit with error.
@@ -754,587 +500,6 @@ def _get_current_project() -> Project:
return project
async def _tell_streaming(
project: Project,
description: str,
name: str | None,
plan_service: Any,
actor: str | None = None,
) -> None:
"""Handle streaming plan generation with real-time progress display.
Args:
project: The project to create the plan in
description: Instructions for the plan
name: Optional plan name
plan_service: PlanService instance
actor: Optional actor override for streaming generation
"""
from rich.live import Live
from rich.text import Text
# Node display names for better UX
node_names = {
"load_context": "Loading context files",
"analyze_requirements": "Analyzing requirements",
"generate_plan": "Generating plan",
"validate": "Validating plan",
}
# Track timing for each node
node_times: dict[str, float] = {}
current_node: str | None = None
start_time = time.time()
# Create status display
status = Text()
status.append("Starting plan generation...\n\n", style="bold cyan")
with Live(status, console=console, refresh_per_second=4) as live:
try:
async for event in plan_service.generate_plan_streaming(
project,
description,
name,
actor=actor,
): # type: ignore[arg-type]
# Extract node name from event
for key in event:
if key != "__end__" and key in node_names:
# Node started
if current_node and current_node in node_times:
elapsed = time.time() - node_times[current_node]
status.append(
f" [green]✓[/green] {node_names[current_node]} "
f"[dim]({elapsed:.1f}s)[/dim]\n"
)
current_node = key
node_times[key] = time.time()
status.append(f" [cyan]⏳[/cyan] {node_names[key]}...\n")
live.update(status)
# Check for completion
if "__end__" in event:
if current_node and current_node in node_times:
elapsed = time.time() - node_times[current_node]
status.append(
f" [green]✓[/green] {node_names[current_node]} "
f"[dim]({elapsed:.1f}s)[/dim]\n"
)
total_time = time.time() - start_time
status.append(
f"\n[green]✓[/green] Plan generated successfully! "
f"[dim]Total: {total_time:.1f}s[/dim]\n"
)
live.update(status)
except Exception as e:
# Get user-friendly error message (without "Exception" class name)
error_msg = str(e) if str(e) else "An unknown error occurred"
# If we were in the middle of a node, show it failed
if current_node and current_node in node_names:
elapsed = time.time() - node_times.get(current_node, time.time())
status.append(
f" [red]✗[/red] {node_names[current_node]} failed "
f"[dim]({elapsed:.1f}s)[/dim]\n"
)
status.append(f"\n[red]Error:[/red] {error_msg}\n")
live.update(status)
# Re-raise the exception so callers can handle errors properly
raise
# Show completion message (only if no exception occurred)
console.print(
Panel(
"[green]✓[/green] Plan created and built\n\n"
f"Description: {description[:100]}"
f"{'...' if len(description) > 100 else ''}\n\n"
"Next steps:\n"
" 1. Review changes with 'agents status'\n"
" 2. Run 'agents apply' to apply changes",
title="Plan Ready",
expand=False,
)
)
@app.command()
def tell(
prompt: Annotated[
str,
typer.Argument(help="Instructions for what you want the AI to do"),
],
name: Annotated[
str | None,
typer.Option("--name", "-n", help="Name for the plan"),
] = None,
actor: Annotated[
str | None,
typer.Option(
"--actor",
help=(
"Actor to use for generation (defaults to the configured default actor)"
),
),
] = None,
stream: Annotated[
bool,
typer.Option("--stream", help="Show real-time progress during plan generation"),
] = False,
) -> None:
"""Create a new plan from natural language instructions.
This command takes your instructions and creates a plan for code changes
that can be built and applied.
Use --stream to see real-time progress as the AI generates the plan.
.. deprecated::
Use ``agents plan use`` for the v3 lifecycle.
"""
import asyncio
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
console.print(
"[yellow]Warning:[/yellow] 'tell' is a legacy command and is deprecated.\n"
"[yellow]WARNING:[/yellow] The legacy and v3 plan workflows are INCOMPATIBLE "
"and cannot be mixed.\n"
"Plans created here cannot be referenced by v3 commands "
"('agents plan execute', 'agents plan apply').\n"
"To use the v3 workflow: 'agents plan use <action> <project>'"
)
try:
container = get_container()
plan_service: PlanService = container.plan_service()
testing_mode = os.getenv("CLEVERAGENTS_TESTING_USE_MOCK_AI", "").lower() in (
"true",
"yes",
"1",
)
with suppress(Exception):
if testing_mode:
container.actor_service().ensure_default_mock_actor()
# Get current project
project = _get_current_project()
if stream:
# Use streaming mode for real-time progress
asyncio.run(
_tell_streaming(
project,
prompt,
name,
plan_service,
actor,
)
)
else:
# Use non-streaming mode (original behavior)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
progress.add_task("Creating plan...", total=None)
plan = plan_service.create_plan(
project=project, prompt=prompt, name=name
)
console.print(
Panel(
f"[green]✓[/green] Plan created: {plan.name}\n\n"
f"Prompt: {plan.prompt[:100] if plan.prompt else ''}"
f"{'...' if plan.prompt and len(plan.prompt) > 100 else ''}\n\n"
f"Next steps:\n"
f" 1. Run 'agents build' to generate changes\n"
f" 2. Run 'agents apply' to apply changes",
title="Plan Created",
expand=False,
)
)
except ValidationError as e:
console.print(f"[red]Validation Error:[/red] {e.message}")
raise typer.Abort() from e
except PlanError as e:
console.print(f"[red]Plan Error:[/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()
def build(
verbose: Annotated[
bool, typer.Option("--verbose", "-v", help="Show detailed output")
] = False,
actor: Annotated[
str | None,
typer.Option(
"--actor",
help=(
"Actor to use for building (defaults to the configured default actor)"
),
),
] = None,
) -> None:
"""Build the current plan to generate code changes.
This command sends the plan and context to the selected actor
(using that actor's stored provider/model metadata) to generate
the actual code changes.
.. deprecated::
Use ``agents plan execute`` for the v3 lifecycle.
"""
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
console.print(
"[yellow]Warning:[/yellow] 'build' is a legacy command and is deprecated.\n"
"[yellow]WARNING:[/yellow] The legacy and v3 plan workflows are INCOMPATIBLE "
"and cannot be mixed.\n"
"Plans created here cannot be referenced by v3 commands.\n"
"To use the v3 workflow: 'agents plan use <action> <project>'"
)
try:
container = get_container()
plan_service: PlanService = container.plan_service()
testing_mode = os.getenv("CLEVERAGENTS_TESTING_USE_MOCK_AI", "").lower() in (
"true",
"yes",
"1",
)
with suppress(Exception):
if testing_mode:
container.actor_service().ensure_default_mock_actor()
# Get current project
project = _get_current_project()
# Build the plan
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
console=console,
) as progress:
task = progress.add_task("Building plan with AI...", total=100)
# Build with progress updates
changes = plan_service.build_plan(
project=project,
progress_callback=lambda p: progress.update(task, completed=p),
actor=actor,
)
if changes:
console.print(
Panel(
f"[green]✓[/green] Plan built successfully!\n\n"
f"Generated {len(changes)} change(s):\n"
+ "\n".join(
f"{c.file_path} ({c.operation})" for c in changes[:5]
)
+ (
f"\n ... and {len(changes) - 5} more"
if len(changes) > 5
else ""
)
+ "\n\nRun 'agents apply' to apply these changes.",
title="Build Complete",
expand=False,
)
)
else:
console.print("[yellow]No changes generated.[/yellow]")
except PlanError as e:
console.print(f"[red]Build Error:[/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
def _lifecycle_apply_with_id(plan_id: str, fmt: str = "rich") -> None:
"""Run the v3 lifecycle apply for a specific plan.
Transitions the plan through:
Execute/complete -> Apply/queued -> Apply/processing -> Apply/applied.
"""
from cleveragents.application.services.plan_lifecycle_service import (
InvalidPhaseTransitionError,
PlanNotReadyError,
)
try:
# Validate ULID format before querying v3 storage. A non-ULID
# identifier (e.g., a legacy plan name) will never be found in v3
# storage; catching it here provides an actionable error message
# instead of a generic "Plan not found".
_validate_plan_ulid(plan_id)
service = _get_lifecycle_service()
# Fail-fast: read-only plans must not enter Apply phase
pre_plan = service.get_plan(plan_id)
if pre_plan is None:
console.print(f"[red]Plan '{plan_id}' not found.[/red]")
raise typer.Abort()
if pre_plan.read_only is True:
console.print(
f"[red]Cannot apply plan '{plan_id}': plan is read-only.[/red]"
)
raise typer.Abort()
from cleveragents.domain.models.core.plan import (
PlanPhase,
ProcessingState,
)
# Determine current phase and drive through apply
if (
pre_plan.phase == PlanPhase.EXECUTE
and pre_plan.state == ProcessingState.COMPLETE
):
# Transition Execute/complete -> Apply/queued
service.apply_plan(plan_id)
current = service.get_plan(plan_id)
if current.phase == PlanPhase.APPLY and current.state == ProcessingState.QUEUED:
service.start_apply(plan_id)
current = service.get_plan(plan_id)
if (
current.phase == PlanPhase.APPLY
and current.state == ProcessingState.PROCESSING
):
service.complete_apply(plan_id)
plan = service.get_plan(plan_id)
# Notify A2A facade for protocol bookkeeping
_notify_facade("plan.apply", {"plan_id": plan_id})
if fmt != OutputFormat.RICH.value:
data = _plan_spec_dict(plan)
console.print(format_output(data, fmt))
else:
_print_lifecycle_plan(plan, title="Plan Applied")
console.print("\n[dim]Plan apply completed successfully.[/dim]")
except InvalidPhaseTransitionError as e:
console.print(f"[red]Invalid transition:[/red] {e}")
raise typer.Abort() from e
except PlanNotReadyError as e:
console.print(f"[red]Plan not ready:[/red] {e}")
raise typer.Abort() from e
except ValueError as e:
# Provider-resolution failures (e.g. missing API key/config) should be
# reported as a controlled CLI error instead of bubbling to a 500.
console.print(f"[red]Execution Error:[/red] {e}")
raise typer.Abort() from e
except CleverAgentsError as e:
console.print(f"[red]Error:[/red] {e.message}")
raise typer.Abort() from e
@app.command()
def new(
name: Annotated[
str,
typer.Argument(help="Name for the new plan"),
],
) -> None:
"""Create a new empty plan and switch to it.
.. deprecated::
Use ``agents plan use`` for the v3 lifecycle.
"""
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
console.print(
"[yellow]Warning:[/yellow] 'new' is a legacy command. "
"Use 'agents plan use <action> [project]' for the v3 lifecycle."
)
try:
container = get_container()
plan_service: PlanService = container.plan_service()
# Create new plan
# Get the current project first
from cleveragents.application.services.project_service import ProjectService
project_service: ProjectService = container.project_service()
current_project = project_service.get_current_project()
if not current_project:
console.print(
"[red]Error:[/red] No project found. Run 'agents init' first."
)
raise typer.Abort()
plan = plan_service.new_plan(project=current_project, name=name)
console.print(f"[green]✓[/green] Created and switched to plan: {plan.name}")
console.print("Use 'agents tell' to add instructions to this plan.")
except ValidationError as e:
console.print(f"[red]Validation Error:[/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()
def current() -> None:
"""Show the current active plan.
.. deprecated::
Use ``agents plan status`` for the v3 lifecycle.
"""
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
console.print(
"[yellow]Warning:[/yellow] 'current' is a legacy command. "
"Use 'agents plan status [plan_id]' for the v3 lifecycle."
)
try:
container = get_container()
plan_service: PlanService = container.plan_service()
# Get current project
project = _get_current_project()
# Get current plan
plan = plan_service.get_current_plan(project=project)
if not plan:
console.print("[yellow]No current plan.[/yellow]")
console.print(
"Create one with 'agents new <name>' or 'agents tell <prompt>'."
)
raise typer.Exit(0)
# Display plan info
info_text = f"""
[bold]Current Plan:[/bold] {plan.name}
[bold]Status:[/bold] {plan.status}
[bold]Created:[/bold] {plan.created_at}
[bold]Prompt:[/bold] {plan.prompt[:200] if plan.prompt else "No prompt set"}"
"{" ... " if plan.prompt and len(plan.prompt) > 200 else ""}"
"""
console.print(Panel(info_text.strip(), title="Current Plan", expand=False))
except CleverAgentsError as e:
console.print(f"[red]Error:[/red] {e.message}")
raise typer.Abort() from e
@app.command()
def cd(
name: Annotated[
str,
typer.Argument(help="Name of the plan to switch to"),
],
) -> None:
"""Switch to a different plan.
.. deprecated::
Use ``agents plan status <plan_id>`` for the v3 lifecycle.
"""
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
console.print(
"[yellow]Warning:[/yellow] 'cd' is a legacy command. "
"Use 'agents plan status <plan_id>' for the v3 lifecycle."
)
try:
container = get_container()
plan_service: PlanService = container.plan_service()
# Get current project
project = _get_current_project()
# Switch to plan
plan = plan_service.switch_to_plan(project=project, name=name)
console.print(f"[green]✓[/green] Switched to plan: {plan.name}")
except ValidationError as e:
console.print(f"[red]Plan not found:[/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("continue")
def continue_plan(
prompt: Annotated[
str | None,
typer.Argument(help="Additional instructions to continue with"),
] = None,
) -> None:
"""Continue working on the current plan.
.. deprecated::
Use ``agents plan use`` for the v3 lifecycle.
"""
from cleveragents.application.container import get_container
from cleveragents.application.services.plan_service import PlanService
console.print(
"[yellow]Warning:[/yellow] 'continue' is a legacy command. "
"Use 'agents plan use <action> [project]' for the v3 lifecycle."
)
try:
container = get_container()
plan_service: PlanService = container.plan_service()
# Get current project
project = _get_current_project()
# Continue the plan
if prompt:
plan_service.continue_plan(project=project, prompt=prompt)
console.print("[green]✓[/green] Added instructions to current plan.")
console.print("Run 'agents build' to generate new changes.")
else:
# Just continue with existing plan
plan = plan_service.get_current_plan(project=project)
if not plan:
console.print("[yellow]No current plan to continue.[/yellow]")
raise typer.Abort()
console.print(f"[green]✓[/green] Continuing with plan: {plan.name}")
console.print("Run 'agents build' to continue building.")
except CleverAgentsError as e:
console.print(f"[red]Error:[/red] {e.message}")
raise typer.Abort() from e
# =============================================================================
# V3 Plan Lifecycle Commands
# =============================================================================
+58 -7
View File
@@ -16,6 +16,8 @@ from __future__ import annotations
import json
import logging
import sys
import threading
from collections import OrderedDict
from pathlib import Path
from typing import Annotated, Any, cast
@@ -47,6 +49,14 @@ _log = logging.getLogger(__name__)
# Reusable --format option description
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
# MCP logger name constant — used in create() and list_sessions() to suppress
# MCP health-check output during structured (non-Rich) CLI output.
_MCP_LOGGER_NAME = "cleveragents.mcp"
# Thread lock for MCP logger level mutations to prevent race conditions when
# multiple CLI commands execute concurrently (e.g., in parallel test runners).
_mcp_logger_lock = threading.Lock()
# ---------------------------------------------------------------------------
# Module-level service accessor (patchable in tests)
# ---------------------------------------------------------------------------
@@ -186,6 +196,14 @@ def create(
agents session create --actor openai/gpt-4
agents session create --format json
"""
# Suppress MCP daemon logger during JSON/YAML output to prevent health check
# messages from interfering with structured output.
mcp_logger = logging.getLogger("cleveragents.mcp")
orig_level = mcp_logger.level
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
with _mcp_logger_lock:
mcp_logger.setLevel(logging.CRITICAL)
try:
# Create session through the service, then notify the A2A
# facade for protocol compliance and telemetry.
@@ -258,6 +276,10 @@ def create(
"Hint: run 'agents init' to initialise the database."
)
raise typer.Exit(1) from exc
finally:
# Restore original MCP logger level
with _mcp_logger_lock:
mcp_logger.setLevel(orig_level)
@app.command("list")
@@ -276,6 +298,14 @@ def list_sessions(
agents session list --format json
agents session list --format table
"""
# Suppress MCP daemon logger during JSON/YAML output to prevent health check
# messages from interfering with structured output.
mcp_logger = logging.getLogger("cleveragents.mcp")
orig_level = mcp_logger.level
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
with _mcp_logger_lock:
mcp_logger.setLevel(logging.CRITICAL)
try:
service = _get_session_service()
sessions = service.list()
@@ -286,6 +316,10 @@ def list_sessions(
"Hint: run 'agents init' to initialise the database."
)
raise typer.Exit(1) from exc
finally:
# Restore original MCP logger level
with _mcp_logger_lock:
mcp_logger.setLevel(orig_level)
if not sessions:
# For machine-readable formats, always emit a structured empty list
@@ -806,6 +840,14 @@ def tell(
bool,
typer.Option("--stream", help="Stream response in real-time"),
] = False,
fmt: Annotated[
OutputFormat,
typer.Option(
"--format",
"-f",
help=_FORMAT_HELP,
),
] = OutputFormat.RICH,
) -> None:
"""Send a message to a session.
@@ -816,6 +858,7 @@ def tell(
agents session tell --session 01HXYZ... "Hello, world"
agents session tell --session 01HXYZ... --actor openai/gpt-4 "Plan a feature"
agents session tell --session 01HXYZ... --stream "Build tests"
agents session tell --session 01HXYZ... --format json "Hello"
"""
try:
service = _get_session_service()
@@ -839,14 +882,22 @@ def tell(
content=assistant_content,
)
if stream:
# Route streaming output through the Rich console so that the
# redaction layer is applied before any content reaches stdout.
# Previously this used sys.stdout.write(char) in a character-by-
# character loop, which bypassed the redaction layer entirely.
from rich.markup import escape as _escape
data = {
"session_id": session_id,
"user_message": prompt,
"assistant_message": assistant_content,
}
console.print(_escape(assistant_content))
if fmt not in (OutputFormat.RICH.value, OutputFormat.COLOR.value):
typer.echo(format_output(data, fmt))
return
if stream:
# Simulate streaming by printing character by character
for char in assistant_content:
sys.stdout.write(char)
sys.stdout.flush()
sys.stdout.write("\n")
else:
from rich.markup import escape
+3 -68
View File
@@ -255,8 +255,6 @@ def _print_basic_help() -> None:
typer.echo(" plan Plan operations (actor required)")
typer.echo(" actor Actor management and defaults")
typer.echo(" init Initialize a project")
typer.echo(" tell Create a plan (shortcut)")
typer.echo(" build Build the current plan")
typer.echo(" apply Apply plan changes")
typer.echo(" db Database migration management")
typer.echo(" auto-debug Auto-debug operations")
@@ -490,65 +488,6 @@ def init(
raise typer.Exit(1) from e
# Shortcuts for most common commands
@app.command()
def tell(
prompt: Annotated[str, typer.Argument(help="Instructions for the AI")],
name: Annotated[str | None, typer.Option("--name", "-n")] = None,
actor: Annotated[
str | None,
typer.Option(
"--actor",
help=(
"Actor to use for generation (defaults to the configured default actor)"
),
),
] = None,
stream: Annotated[
bool,
typer.Option("--stream", help="Show real-time progress during plan generation"),
] = False,
) -> None:
"""Create a plan from instructions (shortcut for 'plan tell')."""
from cleveragents.cli.commands.plan import tell as plan_tell
kwargs: dict[str, Any] = {
"prompt": prompt,
"stream": stream,
}
if name is not None:
kwargs["name"] = name
if actor is not None:
kwargs["actor"] = actor
plan_tell(**kwargs)
@app.command()
def build(
verbose: Annotated[
bool, typer.Option("--verbose", "-v", help="Show detailed output")
] = False,
actor: Annotated[
str | None,
typer.Option(
"--actor",
help=(
"Actor to use for building (defaults to the configured default actor)"
),
),
] = None,
) -> None:
"""Build the current plan (shortcut for 'plan build')."""
from cleveragents.cli.commands.plan import build as plan_build
kwargs: dict[str, Any] = {"verbose": verbose}
if actor is not None:
kwargs["actor"] = actor
plan_build(**kwargs)
@app.command()
def apply(
plan_id: Annotated[
@@ -745,8 +684,6 @@ def main(args: list[str] | None = None) -> int:
"tui", # Textual TUI
"server", # Server connection management
"repo", # Repository indexing management
"tell", # Shortcut for plan tell
"build", # Shortcut for plan build
"apply", # Shortcut for plan apply
"context-load", # Shortcut for context add
"context-add", # Shortcut
@@ -767,17 +704,15 @@ def main(args: list[str] | None = None) -> int:
# Only register heavyweight subcommands when the invoked command
# actually needs them. Lightweight top-level commands (version,
# info, diagnostics, tell, build, apply, context-load, context-add,
# init) are defined directly on `app` and do not require the full
# subcommand tree, avoiding expensive container/service imports.
# info, diagnostics, apply, context-load, context-add, init) are
# defined directly on `app` and do not require the full subcommand
# tree, avoiding expensive container/service imports.
_LIGHTWEIGHT_COMMANDS = frozenset(
{
"version",
"info",
"diagnostics",
"init",
"tell",
"build",
"apply",
"context-load",
"context-add",
@@ -200,8 +200,8 @@ def _rebuild_v3_plans(
# ── 2. Copy data ─────────────────────────────────────────────────
conn.execute(
sa.text(
f"INSERT INTO _v3_plans_new ({_ALL_DATA_COLUMNS}) "
f"SELECT {_ALL_DATA_COLUMNS} FROM v3_plans"
"INSERT INTO _v3_plans_new (" + _ALL_DATA_COLUMNS + ") "
"SELECT " + _ALL_DATA_COLUMNS + " FROM v3_plans"
)
)