Replaced all 28 uses of the deprecated tempfile.mktemp() function with the
atomic tempfile.mkstemp() + os.close(fd) pattern across 16 test infrastructure
and benchmark files. Added import os where it was not already present.
tempfile.mktemp() has been deprecated since Python 2.3 due to a TOCTOU race
condition: between name generation and file creation, another process could
create a file at the predicted path. Under 16-worker parallel test execution,
there was also a non-zero risk of path collisions.
Files modified:
- features/environment.py (3 call sites)
- robot/helper_resource_registry_migration.py (3 call sites)
- robot/helper_plan_phase_migration.py (1 call site)
- robot/helper_plan_persistence_e2e.py (1 call site)
- robot/helper_persistence_lifecycle.py (1 call site)
- robot/helper_db_lifecycle_models.py (1 call site)
- robot/helper_automation_profiles.py (1 call site)
- features/steps/skill_discovery_steps.py (2 call sites)
- features/steps/resource_registry_tables_steps.py (2 call sites)
- features/steps/plan_persistence_steps.py (1 call site)
- features/steps/persistence_robot_alignment_steps.py (1 call site)
- features/steps/garbage_collection_cli_steps.py (3 call sites)
- features/steps/decision_recording_steps.py (1 call site)
- features/steps/coverage_boost_steps.py (1 call site)
- features/steps/cli_streaming_steps.py (1 call site)
- features/steps/automation_profiles_guards_steps.py (1 call site)
- benchmarks/persistence_robot_bench.py (4 call sites)
All 10,640 BDD scenarios pass. No performance regression observed (within
normal run-to-run variance). Zero remaining uses of tempfile.mktemp() in
the codebase.
ISSUES CLOSED: #730
Implement ContainerToolExecutor for delegating tool invocations to
devcontainer environments with full I/O forwarding. Add PathMapper for
bidirectional host/container path translation. Wire container routing
into ToolRunner with graceful fallback when no executor is configured.
Add container_metadata field to ToolInvocation for tracking execution
context.
New modules:
- tool/container_executor.py: ContainerToolExecutor, ContainerConfig,
ContainerMetadata, ContainerExecutionError, ContainerTimeoutError
- tool/path_mapper.py: PathMapper with host_to_container/container_to_host
Modified:
- tool/runner.py: container execution routing via ExecutionEnvironment
- domain/models/core/change.py: container_metadata on ToolInvocation
- tool/__init__.py: new public exports
Review fixes applied:
- Add Alembic migration m6_004 for container_metadata_json column
- Enforce _MAX_OUTPUT_BYTES (50 MiB) truncation in _run_command()
- Fix path traversal in sync_results_to_host (Path.is_relative_to)
- Allow spaces in _looks_like_path() for valid filesystem paths
- Preserve negative exit codes from signal kills in metadata
- Add default=str to json.dumps(invocation.arguments) safety net
- Log warnings when path mapping recursion depth exceeded
- Warn when devcontainer binary not found on PATH
- Use default allow_nan for host-path JSON validation in runner.py
(only container path requires RFC 7159 strict mode)
- Reject URL-like patterns in _looks_like_path() to avoid false
positives on API routes, protocol-relative URIs, and query strings
- Add extract_container_metadata() static helper on
ContainerToolExecutor as bridge for ToolInvocation wiring
- Use raw_stdout bytes in sync_results_to_host to prevent
binary file corruption from text-mode decode/re-encode
- Apply posixpath.normpath() in workspace_folder validator and
reject path components containing '..'
- Check result.timed_out in sync_results_to_host and raise
ContainerTimeoutError instead of always raising ContainerExecutionError
- Detect overlapping host_root/container_root in PathMapper and
raise ValueError to prevent corrupt bidirectional mappings
- Wrap host-side I/O in sync_results_to_host with try/except
OSError to produce ContainerExecutionError on write failure
- Enforce int(timeout) in _build_exec_command to prevent shell
injection via malicious objects with __str__ methods
- Change ToolResult validator from 'not self.error' to
'self.error is None' so empty-string errors are accepted
- Iterate required list directly in ToolRunner schema validation
to detect fields listed in required but absent from properties
Closes#515
Wire per-service retry policies and circuit breakers into the service
layer via ServiceRetryWiring, backed by ServiceRetryPolicyRegistry and
configurable through Settings environment variables.
Production hardening from code review:
- Fix TOCTOU race in CircuitBreaker._on_success (half-open state)
- Add half-open probe limit to prevent unbounded concurrent requests
- Track all exception types for circuit breaker failure counting
- Detect async callables wrapped in functools.partial and callable objects
- Enforce spec-compliant 2s minimum for linear backoff strategy
- Enforce 0.1s floor for fixed backoff strategy
- Add retry amplification guard via contextvars nesting depth tracking
- Cap total retry wall-clock time at 300s (MAX_RETRY_TOTAL_TIMEOUT)
- Sanitize exception messages in retry logs to prevent secret leakage
- Fix wrap_service_method TOCTOU by holding cache lock for full operation
- Deep-copy default policies to prevent cross-policy mutation
- Warn on unknown override keys in apply_overrides
- Guard apply_overrides against non-dict and deeply nested JSON values
- Read circuit breaker state under lock in is_circuit_open
- Catch RecursionError in JSON config parsing
- Add total_timeout + nesting guard to retry_service_operation decorator
- Extend secret sanitization to Authorization headers, private_key,
connection_string, and access_key patterns
- Enforce 0.1s floor on jitter backoff strategy
- Cache wait strategies per service in ServiceRetryWiring (M3)
- Reset failure_count to 0 when entering half-open from open (M6)
- Use cached _get_wait_strategy() in execute()/async_execute()
- Move circuit-open logging out of _on_failure lock scope to prevent
holding the lock during potentially slow I/O (F1)
- Pass total_timeout=MAX_RETRY_TOTAL_TIMEOUT to wrap_service_method
retry_service_operation call for consistency with execute() (F4)
- Capture failure_count into local variable inside lock scope before
logging outside the lock, preventing stale reads from concurrent
threads in CircuitBreaker.call() and async_call() (F1)
- Deep-copy module-level DEFAULT_DATABASE_RETRY and DEFAULT_CIRCUIT_BREAKER
in ServiceRetryPolicyRegistry.get() for auto-generated unknown service
policies, preventing shared mutable state corruption (F1)
- Unify CircuitBreaker to a single threading.Lock for sync and async (P1-1)
- Restore BaseException permit in half-open path to prevent permit leak (P1-6)
- Prevent CircuitBreakerOpen cascading into failure_count (S2)
- Protect all logger calls with contextlib.suppress (S3, S4)
- Replace time.time() with time.monotonic() for monotonic timing (S5)
- Add distinct log events for half-open and closed transitions (S11, S12)
- Track pre-existing services so second apply_settings_defaults only
targets newly registered services (P1-2)
- Lazy circuit breaker creation via _get_or_create_cb() (P1-3)
- Reject async callables in sync execute() with TypeError (P1-5)
- Strengthen retry predicate to retry_if_exception_type(Exception) &
retry_if_not_exception_type(CircuitBreakerOpen) (S1)
- Add lock on _get_wait_strategy cache access (P2-16)
- Truncate raw JSON to 80 chars in override warning (P2-17)
- Warn on non-dict JSON overrides (P2-29)
- Debug log for nesting guard bypass (S13)
- Deep-copy from get() and all_policies() in registry (P1-4)
- Thread-safe registry with threading.Lock (P2-15)
- Robust exception handling in apply_overrides get() (P2-18)
- Log ValidationError details on override failure (P2-19)
- Sanitize service_name via _safe_service_name() (P2-28)
- Warn on non-dict sub-key values in overrides (P2-30)
- Allowlist for is_read_only_plan_operation phases (P2-10)
- Cap retry_auto_debug sleep at 60s (P2-11)
- Use is-not-None instead of falsy checks for error values (P2-12)
- Extend secret regex with bearer, session_id, auth_token,
refresh_token, client_secret patterns (P2-25)
- Pre-truncate error messages to 2000 chars before regex (P2-26)
- Add upper bounds on retry Settings fields (P2-7)
- Add cross-field validator max_delay >= base_delay (P2-8)
- Case-insensitive backoff strategy validation (P2-21)
- Add half_open_max_successes setting (S10)
- Remove phantom ContextFragment from services __all__ (ImportError fix)
- Export ServiceRetryWiring from application.services package
- Include sanitised error context in TypeError logging fallback
- Initialise RetryContext.attempt_count to 1 for bare context-manager usage
- Introduce CircuitBreakerState StrEnum replacing raw string literals
- Fix vacuous CircuitBreakerOpen propagation assertions in BDD steps
- Replace tautological logging test with structlog capture verification
- Assert circuit breaker existence instead of silently skipping on None
- Add Unicode control-char rejection validator to ServiceRetryPolicy.service_name
- Add name parameter with service= in all log calls
- Add extra="forbid" to all 3 Pydantic models
- Deep-copy _SERVICE_DEFAULTS construction
- Key normalisation (.strip()) in get() and apply_overrides()
- Add cooldown <= recovery_timeout validator
- Async guard on RetryContext.execute()
- Nesting guard on RetryContext.execute()/async_execute()
- stop_after_delay(300.0) on RetryContext
- retry_auto_debug async-only guard, dict result fix, sleep guard
- Retry-attempt logging in RetryContext
- Module-level docs for contextlib.suppress(TypeError) rationale
- Exhaustion log on retry failure
- Startup log in __init__; name=service_name to CircuitBreaker
- log_after_retry guarded to not fire on first-attempt success
- get_retry_decorator now includes logging callbacks
- Changed retry_backoff_strategy from str to RetryStrategy StrEnum
Closes#313
Implement TDD bug-capture tests for bug #570 where `agents session create`
fails because `_get_session_service()` calls `container.db()` which does
not exist on the DI Container class (AttributeError). Same root cause as
bug #554.
Behave BDD scenarios tagged @tdd_bug @tdd_bug_570 @tdd_expected_fail
exercise the real DI path (no mocks). Includes Robot Framework integration
smoke tests with self-inverting helper and ASV benchmark baseline.
ISSUES CLOSED: #631
Temporal metadata fields (validFrom, validUntil, isCurrent, isRevisionOf)
on UKO InformationUnit nodes enable revision chain tracking: when code
changes, old nodes are marked historical and new revision nodes are
created with back-links. Three storage tiers (hot/warm/cold) filter
nodes by temporal scope (current/recent/all) with configurable retention
(warm_retention_hours default 24h, cold_retention_days default 90d).
Includes TemporalMetadata, TemporalNode, RevisionChain, TierQueryResult,
TierRetentionConfig frozen domain models, TemporalBackend protocol,
InMemoryTemporalBackend stub, TemporalService with structlog and DI,
BackendSet.temporal typing upgrade from object|None to TemporalBackend|None.
67 Behave scenarios, 8 Robot Framework tests, ASV benchmarks, and
reference documentation.
ISSUES CLOSED: #577
Implement the UKOIndexer service that produces UKO triples from resources
using pluggable domain-specific analyzers, wraps each triple with provenance
metadata, and simultaneously indexes into text, vector, and graph backends.
Key design decisions and components:
- UKOIndexer orchestrates the full index lifecycle: add_resource,
update_resource (remove-then-add), remove_resource, and maintenance
triggers. Each operation fires lifecycle hooks (on_indexed, on_removed,
on_error) so callers can observe progress.
- Analyzer selection is pluggable via ContentAnalyzer protocol. The indexer
accepts a registry mapping resource types to analyzers. PythonAnalyzer
and MarkdownAnalyzer are provided as built-in implementations.
- LocationContentReader protocol abstracts file I/O with a base_dir
parameter for path-traversal prevention (post-resolve validation rejects
paths escaping the base directory and non-regular files).
- UKOTriple model includes a @model_validator ensuring at least one of
object_uri or object_value is populated, preventing empty triples at
construction time.
- Triple removal uses scoped deletion via uko:sourceResource predicate to
avoid shared-subject collision — only triples originating from the
specific resource are removed, not all triples for a shared subject.
- _resource_subjects.pop is deferred until after all backend removal
operations succeed, preventing inconsistent state on partial failure.
- analyzer.analyze() is wrapped in try/except so that analyzer errors
produce an IndexResult with error details rather than propagating
exceptions to callers.
- All lifecycle hook calls are guarded via _fire_on_indexed,
_fire_on_removed, and _fire_on_error helpers that catch and log hook
exceptions without disrupting the indexing pipeline.
- max_triples parameter (default 50,000) bounds analyzer output size to
prevent runaway resource consumption.
- ResourceFileWatcher monitors filesystem paths via watchdog and triggers
re-indexing callbacks on file changes with configurable debouncing.
Emits RESOURCE_MODIFIED domain events via EventBus when file changes
are detected. Debounce timers coalesce rapid edits into a single
callback invocation. Thread-safe design with daemon threads for clean
shutdown.
- SearchResult.__post_init__ validates score is in [0.0, 1.0], correctly
rejecting NaN values.
- Placeholder embedding uses [1.0] instead of [float(len(content))] to
avoid leaking content size information.
- isinstance check on graph_backend ensures GraphIndexBackend protocol
compliance at runtime.
- Test doubles extracted to features/mocks/uko_indexer_mocks.py for reuse
across BDD steps and Robot helpers.
Spec reference: Architecture > ACMS > Real-time Index Synchronization
(specification.md lines ~43205-43300).
ISSUES CLOSED: #578
ActorRegistry._actor_name() built names via f"{provider}/{model}", which
produced names with multiple slashes when providers included models
containing "/" (e.g. OpenRouter's "anthropic/claude-sonnet-4-20250514").
The resulting name violated the spec pattern ^[a-z0-9_-]+/[a-z0-9_-]+$
and triggered a ValidationError during actor upsert.
Now sanitises both provider and model components by replacing "/" with "-"
and lowercasing, so multi-slash provider models no longer break actor
listing.
Includes 6 Behave BDD regression scenarios (covering zero-provider,
multi-slash, consecutive-slash, leading-slash, and name-validation
cases), Robot Framework integration smoke tests, and ASV benchmarks.
ISSUES CLOSED: #592
Behave BDD scenarios (3) tagged @tdd_bug @tdd_bug_592 @tdd_expected_fail
exercise the real ActorRegistry._actor_name() code path with a provider
whose default model contains '/' separators. The tests assert correct
behaviour (exit 0, single-slash names, valid JSON) and fail while the
bug is present; the @tdd_expected_fail handler inverts results so CI
stays green.
Includes Robot Framework integration smoke tests (3), ASV benchmarks (3),
and a shared FakeProviderInfo/FakeProviderRegistry mock in
features/mocks/fake_provider.py.
ISSUES CLOSED: #634
Add TDD regression tests for bug #570 where `_get_session_service()`
calls `container.db()` but the DI `Container` class has no `db`
provider, raising `AttributeError`. Same root cause as bug #554.
Includes 4 Behave BDD scenarios tagged `@tdd_bug @tdd_bug_570
@tdd_expected_fail`, Robot Framework integration smoke tests with
`--format plain`, and ASV service-layer benchmarks. Tests exercise the
real DI path by resetting `_service = None` and using a file-based
SQLite database.
Implements the `@tdd_expected_fail` inversion infrastructure:
- Behave: `after_scenario` hook in `features/environment.py` inverts
pass/fail for scenarios tagged `@tdd_expected_fail`
- Robot: `robot/tdd_expected_fail_listener.py` listener (API v3)
performs the same inversion for Robot test cases
- `noxfile.py`: registers the listener via `--listener` in both the
`integration_tests` and `slow_integration_tests` sessions
Migrates 18 existing TDD scenarios across 5 feature files from the old
`@tdd @bugNNN` convention to the standardised `@tdd_bug @tdd_bug_NNN`
tags per CONTRIBUTING.md § TDD Bug Test Tags.
Refs: #570
Add 10 Behave BDD scenarios (@tdd_bug @tdd_bug_554 @tdd_expected_fail)
for the session list DI wiring bug where _get_session_service() calls
container.db() but the Container has no db provider (AttributeError).
Scenarios cover empty list, format validation (JSON/YAML/plain/rich),
init-then-list lifecycle, and post-create list paths.
Implement @tdd_expected_fail infrastructure: Behave after_scenario hook
inverts FAIL→PASS (expected) and PASS→FAIL (unexpected fix), plus Robot
Framework listener (Listener API v3) with identical semantics registered
via --listener in both integration_tests and slow_integration_tests nox
sessions.
Migrate 18 existing TDD scenarios across 5 feature files from legacy
@tdd @bugNNN convention to @tdd_bug @tdd_bug_NNN per CONTRIBUTING.md
§ TDD Bug Test Tags.
Includes Robot Framework integration smoke tests and ASV service-layer
benchmarks.
Refs: #554
Implement TDD bug-capture tests for bug #570 where `agents session create`
fails because `_get_session_service()` calls `container.db()` which does
not exist on the DI Container class (AttributeError). Same root cause as
bug #554.
Behave BDD scenarios tagged @tdd_bug @tdd_bug_570 @tdd_expected_fail
exercise the real DI path (no mocks). Includes Robot Framework integration
smoke tests with self-inverting helper and ASV benchmark baseline.
ISSUES CLOSED: #631
Implement TDD bug-capture tests for bug #554 where `agents session list`
fails because `_get_session_service()` calls `container.db()` which does
not exist on the DI Container class (AttributeError).
Behave BDD scenarios tagged @tdd_bug @tdd_bug_554 @tdd_expected_fail
exercise the real DI path (no mocks) and assert correct behavior. The
@tdd_expected_fail handler in environment.py inverts failed→passed while
the bug is present, keeping CI green.
Also adds:
- @tdd_expected_fail infrastructure in features/environment.py
(tag validation + status inversion in after_scenario hook)
- behave-parallel exit logic fix to use summary-based failure
detection (compatible with TDD status inversion)
- Robot Framework integration smoke tests with self-inverting helper
- ASV benchmark baseline for session list command throughput
ISSUES CLOSED: #630
Implement the structured metrics collection framework covering 14
operational metric types with proper Histogram, Counter, and Gauge
semantics per the spec (Architecture > Observability > Metrics
Collection, lines ~43805-43825).
Domain layer:
- Add MetricType enum (HISTOGRAM, COUNTER, GAUGE) to metrics.py
- Add MetricDefinition model and METRIC_DEFINITIONS registry mapping
all 14 OperationalMetricKey values to their metric types
- Extend MetricCollector with typed factory methods (histogram, counter,
gauge) and 14 convenience methods (plan_duration, plan_cost,
plan_decision_count, subplan_count, actor_invocation_count,
actor_latency, tool_invocation_count, tool_error_rate,
context_build_time, context_token_count, llm_call_count,
llm_total_tokens, llm_total_cost, llm_avg_latency)
- Extend MetricEntry with optional metric_type field that auto-resolves
from METRIC_DEFINITIONS via MetricCollector.record()
Infrastructure layer:
- Add MetricsEmitter (infrastructure/observability/metrics_emitter.py)
with emit(), emit_batch(), from_settings(), and enabled/disabled
support for structured log emission in local mode
- Add metrics_log_processor (config/metrics_processor.py) for structlog
integration
Configuration:
- Add metrics_enabled and metrics_export_prometheus settings
- Register MetricsEmitter as DI Singleton in application container
Instrumentation:
- Add best-effort metric emission in PlanExecutor for plan_duration
(both runtime and stub execute paths) and plan_decision_count
(strategize path) via _try_emit_metric helper that tolerates invalid
plan IDs in test fixtures
Testing:
- 34 Behave BDD scenarios (features/observability/metrics_collection.feature)
- 8 Robot Framework integration tests (robot/metrics_collection.robot)
- ASV benchmark suite (benchmarks/bench_metrics_collection.py)
Closes#579
Implement StrategyCoordinator and FusionEngine as named facades over
the existing ACMS pipeline components, providing clean public APIs
for parallel strategy execution with proportional budget allocation
and fragment fusion with dedup/conflict resolution/knapsack packing.
Key changes:
- Add StrategyCoordinator with parallel execution and confidence-based budget allocation
- Add FusionEngine with URI+hash dedup, max-depth conflict resolution, greedy knapsack packing
- Add budget overage guard with lowest-relevance fragment dropping
- Add per-strategy max caps enforcement
- Wire into existing ContextAssemblyPipeline
- Add Behave BDD tests, Robot integration tests, ASV benchmarks
- Add docs/reference/acms_fusion.md
ISSUES CLOSED: #192
Implemented lazy container activation for devcontainer-instance resources
with ContainerLifecycleState enum tracking six states (inactive, starting,
active, stopping, stopped, error) with validated transitions. Extended
DevcontainerHandler with devcontainer up CLI integration and JSON output
parsing for container start. Added periodic health checking via
devcontainer exec ping with configurable interval. Added agents resource
stop and agents resource rebuild CLI commands for manual lifecycle
control. Wired session close and plan completion hooks to automatic
container cleanup. Includes lifecycle state persistence in resource
registry with timestamped transitions. Added Behave BDD tests, Robot
integration tests, and ASV activation latency benchmarks.
- Added remoteWorkspaceFolder absolute-path validation
- Aligned spec: handler name, rebuild types, --yes flag on stop/rebuild
- Added registry re-read in stop_container success path for consistency
- Added session_id field to ContainerLifecycleTracker for scoped cleanup
- Scoped stop_all_active_containers to session_id when provided
- Wired _cleanup_devcontainers into fail_apply and fail_execute
- Wired start_health_check into activate_container success path
- Restructured facade session close to always run container cleanup
even without session service (F4)
- Re-read tracker from registry in activate_container success path
- Added evict_terminal_trackers to cap registry growth
- Updated devcontainer_resources.md: health check auto-start, scoped
cleanup hooks, known limitations for eviction and sandbox_strategy
- Wired evict_terminal_trackers into stop_all_active_containers so
terminal-state trackers are actually evicted in production
- Made stop_container idempotent: returns early when container is
already in a terminal state instead of raising ValueError
- Fixed benchmark health check thread leak in TimeActivationLatency
by clearing registry after each timing loop
- Added rebuild pass-through (--reset-container flag to devcontainer up)
- Added host_workspace_path field on ContainerLifecycleTracker so
health probes use the host-side path for devcontainer exec
- Wired lazy activation into DevcontainerHandler.resolve() for
devcontainer-instance resources in non-running states
- Changed _default_strategy from SNAPSHOT to NONE (container
itself provides isolation; SandboxFactory raises NotImplementedError
for snapshot)
- Restricted _STOPPABLE_TYPES to devcontainer-instance only
(container-instance is not directly stoppable via CLI)
ISSUES CLOSED: #514
Add RepoIndexingService with incremental refresh, language detection,
SHA-256 hashing, and policy enforcement. Includes domain models,
DB persistence, DI wiring, Behave/Robot/ASV tests, and reference docs.
ISSUES CLOSED: #195
## Summary
Fix `agents project show` not finding a project immediately after creation. Extends the `session.commit()` fix from #589 to also cover `update()` and `delete()` in `NamespacedProjectRepository`.
## Changes
**Production fix** (`src/cleveragents/infrastructure/database/repositories.py`):
- Add `session.commit()` to `create()`, `update()`, and `delete()` methods
- Add `finally: session.close()` guard to all three methods
- Update class docstring to reflect commit-per-method pattern
**Tests & benchmarks**:
- 3 Behave BDD regression scenarios (`features/project_show_after_create.feature`)
- Robot Framework integration smoke tests with "not found" assertion (`robot/project_show_after_create.robot`)
- ASV benchmarks for create-then-show round-trip (`benchmarks/project_show_after_create_bench.py`)
## Review feedback addressed
- **F1**: Removed unrelated em-dash CHANGELOG edits — wrote clean entry from scratch
- **F2**: Kept Suite Setup/Teardown (required for `${PYTHON}` variable); updated stale docs
- **F3**: Added "not found" string assertion to Robot negative test case
- **F4**: Removed redundant `Base.metadata.create_all()` from `_make_fresh_repo()` helper
- Updated all stale TDD "expected to fail" comments — this PR includes the fix
## Process
- Single squashed commit, rebased onto `master` (no merge commits)
- Prescribed commit message from issue #590 metadata
ISSUES CLOSED: #590
Reviewed-on: cleveragents/cleveragents-core#593
Reviewed-by: Rui Hu <rui.hu@cleverthis.com>
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
## Summary
Fix `agents project create` not persisting projects to the database. `NamespacedProjectRepository.create()` called `session.flush()` but never `session.commit()`, so projects were invisible to subsequent `agents project list` invocations that open a separate session.
## Changes
**Production fix** (`src/cleveragents/infrastructure/database/repositories.py`):
- Replace `session.flush()` with `session.commit()` in `NamespacedProjectRepository.create()`
- Add `finally: session.close()` guard for proper session lifecycle
**Tests & benchmarks**:
- 4 Behave BDD regression scenarios (`features/project_create_persist.feature`)
- Robot Framework integration smoke tests (`robot/project_create_persist.robot`)
- ASV benchmarks for create-then-list round-trip (`benchmarks/project_create_persist_bench.py`)
## Review feedback addressed
- **H3**: Added `finally: session.close()` to `create()` method
- **M1**: Removed redundant `session.flush()` before `session.commit()`
- **M2**: Updated stale TDD "expected to fail" comments — this PR includes the fix
- **M4**: Rewrote CHANGELOG entry to describe the fix, not just tests
- **F1**: Updated Robot documentation (Suite Setup/Teardown kept — needed for `${PYTHON}`)
- **F2**: Fixed bare assertion `"my-app"` to `"local/my-app"` in namespace scenario
- **F3**: Removed redundant `Base.metadata.create_all()` from `_make_fresh_repo()`
## Process
- Single squashed commit, rebased onto `master` (no merge commits)
- Prescribed commit message from issue #589 metadata
ISSUES CLOSED: #589
Reviewed-on: cleveragents/cleveragents-core#591
Reviewed-by: Rui Hu <rui.hu@cleverthis.com>
Co-authored-by: Brent E. Edwards <brent.edwards@cleverthis.com>
Co-committed-by: Brent E. Edwards <brent.edwards@cleverthis.com>
## Summary
Fixes the `plan correct` CLI handler which never passes the decision tree or influence edges to `CorrectionService`, producing single-node impact analysis regardless of the plan's actual decision structure.
Closes#606
## Root Cause
The `correct_decision()` handler in `plan.py` created a bare `CorrectionService()` and called `analyze_impact()` and `execute_correction()` without passing `decision_tree` or `influence_edges`. Both parameters default to `None` -> empty dicts, causing `_compute_affected_subtree()` BFS to return only the single target decision, ignoring all descendants and influence-DAG dependents.
## Changes
### Production Fix (`src/cleveragents/cli/commands/plan.py`)
- Resolve `DecisionService` via `get_container()` (following the pattern used by `plan explain` and `plan tree`)
- Build structural tree adjacency list from `decision_svc.list_decisions(plan_id)` using `parent_decision_id` relationships
- Fetch influence edges from `decision_svc.get_influence_edges(plan_id)`
- Pass both `decision_tree` and `influence_edges` to `svc.analyze_impact()` and `svc.execute_correction()`
### Existing Test Fixups
- Updated 4 step definition files that mock `CorrectionService` to also mock the new `DecisionService` resolution path
### New Tests
- **Behave BDD**: 3 scenarios verifying tree/edge forwarding (dry-run subtree, execution subtree, leaf node)
- **Robot Framework**: 3 integration smoke tests
- **ASV Benchmark**: Tree building and analyze_impact overhead benchmarks
## Quality Gates
- `nox -s lint` — PASSED
- `nox -s typecheck` — 0 errors
- `nox -s unit_tests` — 9,109 scenarios, 0 failures
- `nox -s coverage_report` — 97%
ISSUES CLOSED: #606
Reviewed-on: cleveragents/cleveragents-core#639
Co-authored-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Co-committed-by: Jeffrey Phillips Freeman <jeffrey.freeman@cleverthis.com>
Implement spec-mandated pre-flight guardrail checks that validate plan
readiness before entering the Strategize phase:
1. Action schema validation — verifies action exists and is well-formed.
2. Actor availability — confirms all 4 actor roles registered.
3. Skill/tool existence — transitively resolves all tools/skills.
4. Automation policy — verifies profile permits execution.
5. Rollback feasibility — ensures all tools checkpointable if required.
6. Resource accessibility — shallow connectivity check on linked resources.
7. Validation attachment resolution — pre-resolves all applicable validations.
PlanPreflightGuardrail service runs all 7 checks via run_all_checks().
On first failure, raises PreflightRejection with check name and message.
Wired into plan_lifecycle_service before Strategize phase.
Behave BDD: 18 scenarios covering all 7 checks (positive + negative).
Robot Framework: 3 integration smoke tests.
ASV benchmarks: pre-flight check execution time.
ISSUES CLOSED: #582
ToolRegistryRepository.create(), .update(), and .delete() called
session.flush() but never session.commit(). The CLI factory creates a
raw sessionmaker without a UnitOfWork wrapper, so the transaction was
never committed and SQLAlchemy performed an implicit rollback when the
session was garbage-collected.
The same bug existed in ValidationAttachmentRepository.attach() and
.detach().
Changes:
- Add session.commit() after session.flush() in all five mutating
methods across ToolRegistryRepository and
ValidationAttachmentRepository.
- Add finally: session.close() to guarantee session cleanup regardless
of success or failure.
- Update class docstrings to reflect the new commit-on-write semantics.
- Add Behave BDD feature (tool_add_persist.feature) with scenarios for
single-tool round-trip, multi-tool persistence, and duplicate
rejection, using file-based SQLite to reproduce the cross-session
issue.
- Add Robot Framework integration test (tool_add_persist.robot) with
add-then-list and fresh-list-empty scenarios.
- Add ASV benchmark (tool_add_persist_bench.py) with
track_list_after_add_count metric.
Key decisions:
- File-based SQLite (not in-memory) is used in tests because the bug
only manifests when the session/engine is fully disposed between add
and list, simulating separate CLI invocations.
- Step patterns are prefixed with "tool-persist" to avoid AmbiguousStep
collisions with existing tool_registry_steps.py.
- The commit-in-repository approach was chosen over adding a UnitOfWork
to the CLI factory because the CLI commands are simple CRUD operations
that should auto-persist without requiring callers to remember to
commit.
ISSUES CLOSED: #621
Wire SkillService to use SkillRepository for database persistence,
fixing the bug where `agents skill add` stored skills only in an
in-memory OrderedDict that was lost when the CLI process exited.
Changes:
- SkillService now accepts optional skill_repo and session_factory
parameters. When provided, add_skill() persists to the database
and the constructor pre-loads existing skills from DB rows.
- _get_skill_service() in the CLI now creates a DB-backed service
following the same engine/sessionmaker/repository pattern used by
the tool CLI (tool.py).
- _reset_skill_service() now installs a fresh in-memory SkillService
(instead of setting None) to avoid DB side-effects during unit
testing with parallel runners.
- remove_skill() also persists the deletion to the database.
Test coverage:
- Behave BDD: features/skill_add_persist.feature (4 scenarios)
- Robot Framework: robot/skill_add_persist.robot (3 smoke tests)
- ASV benchmark: benchmarks/skill_add_persist_bench.py
ISSUES CLOSED: #620
Implemented the remaining ACMS pipeline components and advanced context
strategies:
Pipeline Phase 3:
- FragmentOrdererProtocol + RelevanceCoherenceOrderer: orders fragments
by relevance while maintaining narrative coherence via UKO node prefix
grouping. Groups related fragments together, sorts groups by max
relevance, and within groups orders by relevance desc / depth asc.
- PreambleGeneratorProtocol + ProvenancePreambleGenerator: generates
provenance preamble with strategy contributions (fragment counts and
token percentages), confidence indicators (avg/min/max), tier and
depth distribution, UKO node coverage, and coverage gap detection.
Advanced Strategies:
- ArceStrategy (quality 0.95): adaptive recursive context expansion with
iterative multi-backend refinement and configurable iteration limit
(default 5) to prevent unbounded refinement. Uses composite scoring
(relevance + depth + diversity) with contextual boosting for fragments
related to the current top-ranked anchor set.
- TemporalArchaeologyStrategy (quality 0.5): historical context retrieval
from graph+cold backends. Prioritises cold-tier fragments using a
temporal scoring model (tier bonus + relevance + depth).
- PlanDecisionContextStrategy (quality 0.7): decision history retrieval
from warm/cold backends. Prioritises warm then cold tier fragments
for correction and retry scenarios.
All strategies registered in strategy registry with correct quality scores
and backend requirements. All components implement their respective
Protocol interfaces and can be injected into the ContextAssemblyPipeline
via constructor dependency injection.
Tests:
- 33 BDD scenarios in features/acms_pipeline_phase3.feature
- Robot Framework integration tests in robot/acms_pipeline_phase3.robot
- ASV performance benchmarks in benchmarks/acms_pipeline_phase3_bench.py
ISSUES CLOSED: #545
Added minimal LSP server entrypoint supporting initialize/shutdown/exit
handshake over JSON-RPC stdin/stdout transport with Content-Length
framing. Unsupported methods return MethodNotFound error with descriptive
message. Wired LSP requests through ACP facade in local mode. Added
agents lsp serve CLI command with --log-level flag, PID output, and
startup banner. Created reference documentation for the stub server.
Includes Behave BDD tests for protocol handshake, Robot smoke test, and
ASV startup latency benchmark.
ISSUES CLOSED: #203
Core domain types (FragmentProvenance, ContextFragment, ContextBudget,
ContextPayload) now extend their CRP counterparts via Pydantic v2
inheritance, ensuring isinstance compatibility across the model
hierarchy.
Key changes:
- CRP base types made frozen=True (no consumer mutates them)
- CRP AssembledContext fields changed from list to tuple (frozen consistency)
- Core types extend CRP bases: FragmentProvenance(CRPFragmentProvenance),
ContextFragment(CRPContextFragment), ContextBudget(CRPContextBudget),
ContextPayload(CRPAssembledContext)
- Removed duplicate ContextFragment dataclass from skeleton_compressor
- Updated project_context.py to pass tuples to frozen AssembledContext
- Added Behave tests (10 scenarios), Robot integration tests (3 cases),
and ASV benchmarks for the unified hierarchy
- Updated Known Limitations table in docs/reference/acms.md
ISSUES CLOSED: #569
Implemented the analyzer plugin framework with AnalyzerProtocol,
AnalyzerRegistry for registration/discovery by file extension,
PythonAnalyzer (AST-based extraction of modules, classes, functions,
imports, docstrings), and MarkdownAnalyzer (section, code block, and
link extraction). Both analyzers produce well-formed UKO triples with
proper URI schemes.
ISSUES CLOSED: #551
Add the Depth/Breadth Projection System and Skeleton Context
Propagation as specified in docs/specification.md §25265-25340
and §43057-43128:
- ProjectionSpec: frozen Pydantic model capturing a projection
request (focus, breadth, depth, gradient, domain)
- ProjectedNode: frozen model for materialized graph nodes with
resolved depth and distance
- DepthBreadthProjector: stateless BFS projector over UKO graph
adjacency with depth gradient (linear reduction by distance)
- PlanContextInheritance: service computing child plan context
from parent assembled context with skeleton injection
- ChildContextResult: frozen result model with request and skeleton
- InheritanceConfig: frozen config for skeleton_ratio (default 0.2)
- Built-in DetailLevelMap presets for code, docs, and database
Includes 27 Behave BDD scenarios, 9 Robot Framework integration
tests, and ASV benchmarks for all components.
ISSUES CLOSED: #544
Add production-grade Phase 2 (Fragment Fusion) components for the ACMS
context assembly pipeline, replacing the no-op defaults:
- ContentHashDeduplicator: Groups fragments by UKO node URI, hashes
content to detect duplicates, retains highest relevance_score.
- MaxDepthResolver: Resolves depth conflicts by keeping the highest
detail depth per UKO node, with relevance tiebreaking.
- WeightedCompositeScorer: Computes composite score from configurable
weighted factors (relevance=0.4, hierarchy=0.3, quality=0.2,
recency=0.1). Stores component breakdown in metadata.
- GreedyKnapsackPacker: Greedy knapsack selection with depth fallback
(tries depths [9,4,2,0] for oversized fragments) and minimum
fragment token threshold (10).
Also adds:
- ScoredFragment frozen Pydantic model (spec §42825) with
composite_score, score_components, and fragment reference
- score_detailed() method on WeightedCompositeScorer returning
ScoredFragment objects for callers needing full breakdowns
- All components implement v1 Protocol signatures from acms_service.py
and can be DI-injected into ACMSPipeline constructor
Testing:
- 31 Behave BDD scenarios in acms_pipeline_phase2.feature covering
deduplication, depth resolution, scoring, packing, depth fallback,
budget constraints, pipeline integration, and ScoredFragment model
- 6 Robot Framework integration smoke tests
- ASV benchmark suites for all 4 components and ScoredFragment
Quality gates: lint, typecheck (0 errors), unit_tests (8555 scenarios),
coverage (97.0%), dead_code — all passing.
ISSUES CLOSED: #540