ContextTierService started empty on every CLI invocation, so the LLM
received zero file context during plan execution (bug #1028).
- Add context_tier_hydrator.py: reads files from linked project resources
(via git ls-files or os.walk) and stores them as TieredFragment objects
in the tier service. Respects max file size (256 KB), total budget
(10 MB), binary exclusion, and .git/node_modules/__pycache__ skipping.
- Wire hydration into LLMExecuteActor.execute() via lazy import (avoids
M1 E2E regression from top-level import).
- Inject tier_service, project_repository, resource_registry into
LLMExecuteActor from the DI container in _get_plan_executor().
- Add tier_service property to ExecutePhaseContextAssembler.
- Suppress exc_info traceback rendering in context warnings to prevent
false-positive crash detection in M1 E2E tests.
- Add sandbox file-writing support in plan apply (path traversal guards,
protected directory skipping).
- Add 6 Behave scenarios for context tier hydration.
Closes#1028
## Summary
`agents plan use` crashed with `sqlite3.IntegrityError: UNIQUE constraint failed: action_arguments.action_name, action_arguments.name` when the action had arguments already registered via `action create`. The root cause was `ActionRepository.update()` using SQLAlchemy's relationship `.clear()` + `.append()` pattern, which deferred the DELETE and processed the INSERT first — triggering a UNIQUE constraint violation when the same `(action_name, name)` pair was being re-inserted.
## Approach
Replace the `.clear()` + `.append()` pattern with explicit bulk `sa_delete()` + `session.flush()` before re-inserting child rows for both `action_arguments` and `action_invariants`. After the flush, expire the relationship collections with `session.expire(row, ["arguments_rel", "invariants_rel"])` so SQLAlchemy reloads from the now-empty database state before appending replacements. This avoids stale identity map references and guarantees the DELETE is committed before any INSERT.
## Key Changes
### Bug fix (`src/cleveragents/infrastructure/database/repositories.py`)
- `ActionRepository.update()` now uses `sa_delete(ActionArgumentModel)` and `sa_delete(ActionInvariantModel)` with `synchronize_session=False`, followed by `session.flush()`, before re-inserting child rows.
- Targeted `session.expire(row, ["arguments_rel", "invariants_rel"])` replaces the removed `.clear()` calls to force collection reload.
### Schema parity (`src/cleveragents/infrastructure/database/models.py`)
- Added `UniqueConstraint("action_name", "position")` to `ActionInvariantModel`.
- Added `UniqueConstraint("action_name", "name")`, `CheckConstraint` for `arg_type`, and `CheckConstraint` for `requirement` to `ActionArgumentModel`.
### Alembic migration (`alembic/versions/a5_006_action_invariants_unique_constraint.py`)
- New migration adds all four constraints to both `action_invariants` and `action_arguments` tables.
- Includes deduplication guards and data normalization so the upgrade succeeds on existing databases with invalid or duplicate rows.
- Uses `batch_alter_table` for SQLite compatibility.
### Tests
- **Behave** (`features/plan_use_action_args_integrity.feature`): 6 scenarios covering the core bug path, zero-argument regression, multiple arguments, reusable action double-use, non-reusable action archival with invariants, and direct repository update.
- **Robot** (`robot/plan_use_action_args_integrity.robot`): Integration test mirroring the Behave scenarios via a helper script.
- **Shared factory** (`features/mocks/test_uow_factory.py`): Extracted `build_test_uow()` from both test suites into a single shared module to eliminate duplication (DRY).
### Minor
- Updated `src/cleveragents/domain/repositories/__init__.py` docstring from table format to bullet list (conflict resolution from rebase).
Closes#4174
Reviewed-on: cleveragents/cleveragents-core#4197
Reviewed-by: HAL 9000 <HAL9000@cleverthis.com>
Co-authored-by: Rui Hu <rui.hu@cleverthis.com>
Co-committed-by: Rui Hu <rui.hu@cleverthis.com>
Added skeleton_fragments: tuple[ContextFragment, ...] field to ContextPayload in context_fragment.py
- Enables carrying compressed skeleton fragments along with normal context.
Extended ACMSPipeline.assemble() in acms_service.py
- Introduced skeleton_ratio: float = 0.15 (default matching spec) and parent_fragments: tuple[ContextFragment, ...] | None = None parameters.
- These same parameters are also added to ContextAssemblyPipeline.assemble() in acms_pipeline.py for consistency.
Skeleton compression integration
- In Phase 3 of both assemble() methods, computed skeleton_budget = int(budget.available_tokens * skeleton_ratio) and invoked self._skeleton_compressor.compress(parent_fragments, skeleton_budget).
- Compressed skeleton fragments are included in the returned ContextPayload.skeleton_fragments, enabling propagation of skeleton context to child plans.
Tests and behavior coverage
- Added a TDD issue-capture Behave scenario (@tdd_issue @tdd_issue_3563) to demonstrate the fix.
- Added four Behave unit test scenarios asserting: compressor invocation, correct arguments, skeleton presence in output, and skeleton_ratio budget enforcement.
- Added a Robot Framework integration test: parent plan accumulates context → child plan spawned → child plan context contains non-empty skeleton.
- Added skeleton-context-inheritance command to helper_acms_pipeline.py to support testing and manual verification.
Key design decisions
- skeleton_ratio defaults to 0.15 to align with the spec's --skeleton-ratio default.
- parent_fragments is None by default to maintain backward compatibility (no skeleton compression when no parent context).
- skeleton_budget is computed as skeleton_budget = int(budget.available_tokens * skeleton_ratio), deriving the skeleton budget from the total token budget.
- Both ACMSPipeline and ContextAssemblyPipeline are fixed to maintain consistency across the codepath.
ISSUES CLOSED: #3563
Route the 'agents actor add' CLI command through ActorRegistry.add() instead
of the legacy registry.upsert_actor() path. This ensures the original YAML
text, schema_version, and compiled_metadata are preserved in the database.
Changes:
- src/cleveragents/cli/commands/actor.py: Add _load_config_text() helper that
returns both raw text and parsed dict. Refactor add() to call registry.add()
with the raw yaml_text and update=update_existing flag when a registry is
available. The service fallback path (no registry) is unchanged.
- features/steps/actor_cli_steps.py: Update add command step definitions to
mock registry.add() instead of registry.upsert_actor(). Update 'the actor
add should pass the loaded config' assertion to verify registry.add() is
called with a non-empty yaml_text string.
- features/steps/actor_cli_yaml_steps.py: Update add command steps to mock
registry.add() instead of registry.upsert_actor().
- features/steps/actor_add_rich_output_steps.py: Update add command steps to
mock registry.add() instead of registry.upsert_actor().
- robot/helper_actor_add_rich_output.py: Update helper to mock registry.add()
instead of registry.upsert_actor().
- features/actor_add_yaml_first_path.feature: New Behave feature verifying
the YAML-first persistence path is used by actor add.
- features/steps/actor_add_yaml_first_path_steps.py: Step definitions for
the new YAML-first path feature.
- robot/actor_add_yaml_first_path.robot: New Robot integration tests verifying
yaml_text is preserved and upsert_actor is not called.
- robot/helper_actor_add_yaml_first_path.py: Helper script for Robot tests.
Fixes#3426
ISSUES CLOSED: #3426
Implements all four automatic checkpoint triggers defined in the specification
for the Execute phase of the plan lifecycle:
- before_tool_execute: Checkpoint created before any tool with writes=True runs
- after_tool_execute: Checkpoint created after a write tool completes successfully
- on_subplan_spawn: Checkpoint created immediately after a child plan is spawned
- on_error: Checkpoint created after any unrecoverable error in the Execute phase
Changes:
- tool/runner.py: Added optional CheckpointService and auto_checkpoint_triggers
parameters to ToolRunner. Checkpoint hooks fire around write-tool execution
when a CheckpointService is wired. Exported DEFAULT_AUTO_TRIGGERS as a public
constant (single source of truth). Made is_trigger_active() public so callers
can query the active trigger set without accessing private attributes.
- application/services/subplan_execution_service.py: Added optional
CheckpointService, auto_checkpoint_triggers, and parent_plan_id parameters.
on_subplan_spawn checkpoint fires in _execute_one_with_retry before the
first execution attempt. Now imports DEFAULT_AUTO_TRIGGERS from runner.py
(DRY fix).
- application/services/plan_executor.py: Added _is_auto_trigger_active() helper
and on_error checkpoint hooks in both _run_execute_with_stub() and
_run_execute_with_runtime() error paths. Delegates to
ToolRunner.is_trigger_active() instead of accessing private attributes
(module boundary fix).
- application/services/config_service.py: Registered new config key
core.checkpoints.auto_create_on (default: all four triggers enabled) with
env var CLEVERAGENTS_CHECKPOINT_AUTO_CREATE_ON.
- application/services/llm_actors.py: Replaced Any type for lifecycle_service
with PlanLifecycleProtocol (typed Protocol) and tool_runner with ToolRunner
type annotation. Eliminates Any usage for injected dependencies.
Tests:
- features/checkpoint_auto_triggers.feature: 15 Behave scenarios covering all
four triggers, disable-trigger behavior, no-checkpoint-service fallback, and
config key registration.
- features/steps/checkpoint_auto_triggers_tool_steps.py: Step definitions for
ToolRunner and config service scenarios (split from original 519-line file).
- features/steps/checkpoint_auto_triggers_executor_steps.py: Step definitions
for SubplanExecutionService and PlanExecutor scenarios (split from original).
Closes#3439
ISSUES CLOSED: #3439