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
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+2
-2
@@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user