diff --git a/CHANGELOG.md b/CHANGELOG.md index a0734842d..dcd7ca30e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,58 +1,18 @@ # Changelog -# Changelog - -## Unreleased - -- Hardened the TDD bug-fix quality gate for issue #629: PR parsing now - requires whole-word closing keywords (avoids false positives like - "prefixes #12"), TDD bug tag discovery now uses exact token matching - (avoids `@tdd_bug_42` matching `@tdd_bug_420`), and gate evaluation now - requires expected-fail tag removal to be present in the PR diff. CI - integration now fetches the PR base branch for diff analysis and includes - `tdd_quality_gate` in `status-check` requirements for pull requests. - Review-round fixes: `check_expected_fail_removed` now uses word-boundary - matching via `_contains_tag_token` (avoids false positives on partial tag - names); diff expected-fail removal detection now tracks flags at file level - instead of per-hunk (fixes false negatives when tags span different hunks); - `parse_bug_refs` filters out issue number zero; redundant double error - reporting eliminated; regex compilation cached via `lru_cache`; nox session - no longer installs the full project (script uses stdlib only); CI checkout - uses `fetch-depth: 0` for reliable merge-base resolution. - Review-round 2 fixes: diff expected-fail removal detection now requires the - removed line to contain both the expected-fail tag and the specific bug tag - (fixes false positives when two bugs' TDD tests reside in the same file); - `check_expected_fail_removed` error messages now use the correct tag prefix - per file type (`@tdd_bug_N` for `.feature`, `tdd_bug_N` for `.robot`); - `bool` values are now rejected by bug-number validation guards; file-read - error handling in `find_tdd_tests` and `check_expected_fail_removed` now - catches `UnicodeDecodeError` (root-safe unreadable-file handling); temp - directory cleanup added to `after_scenario` hook; 8 new Behave scenarios - covering bool guards, co-located bug false-positive, `run_quality_gate` - argument validation, and `main()` CLI entry point. - Review-round 3 fixes: synthetic PR diff helper now auto-detects `.robot` - vs `.feature` file type and generates the matching diff format (fixes - under-tested robot-format diff code path); `check_expected_fail_removed` - test step now filters files by bug tag via `find_tdd_tests` before - checking (matches the production code path); `after_scenario` temp - directory cleanup no longer sets `context.temp_dir = None` (fixes - cleanup conflict with `cli_init_yes_flag_steps.py`); 2 new Behave - scenarios covering multi-line PR description parsing and non-string - `pr_diff` type guard. -- Added Fix-then-Revalidate orchestration loop for required validations: - bounded retry with configurable limits (0--100 per Safety Profile), - strategy revision escalation via ``auto_strategy_revision`` float - threshold, user escalation via ``needs_user_escalation`` result flag, - and domain events (``VALIDATION_FIX_ATTEMPTED``, ``VALIDATION_FIX_SUCCEEDED``, - ``VALIDATION_FIX_EXHAUSTED``). Validation errors are treated as required - failures regardless of mode. Includes ``auto_validation_fix`` threshold, - per-resource retry tracking, early-exit signalling via ``None`` return from - ``FixCallback``, event bus circuit breaker with lock-protected failure - counter, spec-required ``validation_summary`` and - ``final_validation_results`` fields on the result model, DI container +All notable changes to this project will be documented in this file. +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +- **`task-implementor` posts work-started notification comments** (#11031): Both + the `issue_impl` and `pr_fix` procedures now post an informational "work + started" comment to the Forgejo issue/PR before beginning implementation. + The comment includes the issue/PR title, procedure type, and expected + duration. Posted asynchronously ("fire and move on") so it does not block + the workflow. Step numbering in both procedures has been re-numbered to + accommodate the new step. + - **`agents session tell` invokes real LLM orchestrator actor** (#5784): Replaced the M3 echo-stub with real actor invocation via `SessionWorkflow`, routing through `LangChainSessionCaller` → `ToolCallingRuntime.run_tool_loop()`. The user prompt @@ -74,9 +34,25 @@ ### Added +- **`pr-review-worker` review-started notification** (#11028): The `first_review` + and `re_review` modes now post a "review started" notification comment to the + PR at the beginning of the review, giving PR authors immediate visibility + that a review is in progress. Posted asynchronously so it does not block + the workflow. + - **Plan Rollback Command** (#8557): Implemented `agents plan rollback []` for checkpoint-based plan state restoration in Epic #8493. The command restores a plan's sandbox to the state captured at a given checkpoint, discarding all decisions made after that checkpoint. The checkpoint can be specified as an optional positional second argument or via the `--to-checkpoint` named option. Supports `--yes/-y` flag to skip confirmation prompts and `--format/-f` for output format selection (rich/plain/json/yaml). Included with comprehensive BDD test coverage (>= 97%) and spec-aligned output formatting showing rollback summary, changes reverted, impact analysis, and post-rollback state panels. ### Fixed +- Python 3.13 asyncio compatibility (issue #11134): replaced all deprecated ``asyncio.get_event_loop()`` calls with ``get_running_loop()`` + exception-safe fallback, updated ``datetime.utcnow()`` usage to ``datetime.now(timezone.utc)``, and added ``PYTHONWARNINGS=ignore::DeprecationWarning`` safety net in nox session. Fixes seven failing BDD scenarios caused by Python 3.13 deprecation warnings becoming errors during test execution across base.py, nodes.py, bridge.py, route_bridge.py, reactive_application_coverage_steps.py, langgraph_bridge_steps.py, and route_bridge_coverage_steps.py. +- **Guard cleanup_stale against execute/processing and execute/complete plans** (#11121): + ``_create_sandbox_for_plan()`` in ``src/cleveragents/cli/commands/plan.py`` now + skips ``GitWorktreeSandbox.cleanup_stale()`` when the plan is in + ``execute/processing`` (execution in progress) or ``execute/complete`` (execution + finished, awaiting apply) state. Previously, re-invoking ``agents plan execute`` + on a completed plan would silently destroy the ``cleveragents/plan-`` git + worktree branch, causing ``plan apply`` to merge zero artifacts. The guard + preserves the branch per spec (§sandbox.cleanup defaults to ``on_apply``). + - **Global CLI options ``--data-dir``, ``--config-path``, and ``-v`` now work correctly** (#6785): These spec-required flags were absent from ``main_callback()`` in ``src/cleveragents/cli/main.py``, causing any invocation with these flags to crash @@ -86,6 +62,13 @@ sets ``CLEVERAGENTS_CONFIG_PATH`` for ``ConfigService`` to pick up, and ``-v`` (repeatable count) maps to the appropriate ``structlog`` log level. +- **Add regression test for ActorRegistry.add() nested provider/model extraction** (#4321): + Added BDD regression test confirming that provider and model are correctly + extracted from nested `actors..config` blocks when `type` is only at + the nested level and `name` uses the `local/` namespace prefix. This + scenario was previously fixed in #4300; the test ensures the fix remains + in place and prevents future regressions. + - **Actor configuration validation incorrectly requires top-level provider field** (#4300): Actor configuration in V3 is now obtained from the nested configuration parameter, according to the specification. @@ -327,6 +310,16 @@ ### Fixed +- **`invariant_enforced` decisions not propagated to child plans on subplan spawn** (#9131): + Fixed `SubplanService.spawn()` to propagate all `invariant_enforced` decisions from the + parent plan's decision tree to each child plan's decision tree. Previously, child plans + started Strategize with a completely empty invariant set, violating the spec requirement: + "recorded as `invariant_enforced` decisions that propagate to child plans." The fix adds + a `_propagate_invariant_decisions()` helper that re-records each parent + `invariant_enforced` decision on the child plan, including `non_overridable` global + invariants. BDD regression coverage added in + `features/tdd_invariant_propagation_subplan.feature`. + - **fix(repositories): derive PlanResult.success from result_success column instead of error_message** (#7501): Fixed a critical bug in `PlanRepository._to_domain` where `PlanResult.success` was incorrectly derived from `error_message is None`. Because `error_message` is shared between the build phase @@ -915,5 +908,4 @@ - **TUI -- Permission Question Widget**: A new inline `PermissionQuestionWidget` renders permission requests directly in the conversation stream for single-file operations. Users can allow/reject with single-key shortcuts (`a`/`A`/`r`/`R`), - navigate with arrow keys, confirm with `Enter`, or press `v` to open the full - + navigate with arrow keys, confirm with `Enter`, or press `v` to open the full \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index dbef154a9..e5afcede7 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,6 +8,12 @@ * Luis Mendes * Rui Hu +## Contributors + +### Code Contributions + +- **[asyncio and Deprecation Fixes](https://git.cleverthis.com/cleveragents/cleveragents-core/issues/11134)** - Replaced deprecated `asyncio.get_event_loop()` calls with `get_running_loop()` + safe fallback, updated `datetime.utcnow()` usage to `datetime.now(timezone.utc)`, and added PYTHONWARNINGS safety net in nox session. Fixed seven failing BDD scenarios under Python 3.13 (issue #11134). + # Details Below are some of the specific details of various contributions. diff --git a/features/steps/langgraph_bridge_steps.py b/features/steps/langgraph_bridge_steps.py index ffae58b9e..54daae301 100644 --- a/features/steps/langgraph_bridge_steps.py +++ b/features/steps/langgraph_bridge_steps.py @@ -116,7 +116,10 @@ def step_impl(context): graph.execute = exec_with_messages executor = bridge._create_graph_executor({"graph": "test_graph"}) - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() def _run_operator(message): results = [] @@ -146,6 +149,10 @@ def step_impl(context): except ValueError: context.results.append("graph_executor_error") + # Close the fresh event loop to prevent resource leak + with __import__("contextlib").suppress(RuntimeError): + loop.close() # Safe: get_running_loop catches if another loop already exists + @when("I exercise state utilities and node operator") def step_impl(context): diff --git a/features/steps/reactive_application_coverage_steps.py b/features/steps/reactive_application_coverage_steps.py index 5cc20e300..5b18bc481 100644 --- a/features/steps/reactive_application_coverage_steps.py +++ b/features/steps/reactive_application_coverage_steps.py @@ -2,6 +2,7 @@ from __future__ import annotations + import asyncio import logging import tempfile @@ -29,7 +30,10 @@ def _write_config_file(data: dict[str, Any]) -> Path: def _run_async(coro): try: - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() if loop.is_closed(): raise RuntimeError if loop.is_running(): diff --git a/features/steps/route_bridge_coverage_steps.py b/features/steps/route_bridge_coverage_steps.py index 01c222002..586b56f35 100644 --- a/features/steps/route_bridge_coverage_steps.py +++ b/features/steps/route_bridge_coverage_steps.py @@ -2,6 +2,7 @@ from __future__ import annotations +import time import asyncio from pathlib import Path @@ -153,7 +154,10 @@ def _ensure_topological_levels(graph): def _run_async(coro): try: - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() if loop.is_closed(): raise RuntimeError if loop.is_running(): @@ -435,7 +439,7 @@ def step_graph_route_idle_time(context): @given("I have a graph state that has been idle too long") def step_graph_state_idle(context): context.graph_state = GraphState() - current_time = asyncio.get_event_loop().time() + current_time = time.time() context.graph_state.metadata = {"last_updated": current_time - 10.0} context.graph_state.messages = [] @@ -673,7 +677,11 @@ def step_attempt_downgrade(context): ) return try: - context.downgrade_result = asyncio.get_event_loop().run_until_complete( + try: + _loop_downgrade_result = asyncio.get_running_loop() + except RuntimeError: + _loop_downgrade_result = asyncio.new_event_loop() + context.downgrade_result = _loop_downgrade_result.run_until_complete( context.route_bridge.downgrade_graph_to_stream( context.route_config, context.langgraph ) @@ -709,7 +717,11 @@ def step_route_upgrade_downgrade(context): @when("I perform an upgrade and then downgrade cycle") def step_cycle_upgrade_downgrade(context): - context.langgraph = asyncio.get_event_loop().run_until_complete( + try: + _loop_langgraph = asyncio.get_running_loop() + except RuntimeError: + _loop_langgraph = asyncio.new_event_loop() + context.langgraph = _loop_langgraph.run_until_complete( context.route_bridge.upgrade_stream_to_graph( context.route_config, context.stream_message ) @@ -731,7 +743,15 @@ def step_cycle_upgrade_downgrade(context): name=name, type=getattr(node, "type", NodeType.FUNCTION) ) - context.stream_config = asyncio.get_event_loop().run_until_complete( + try: + + _loop_stream_config = asyncio.get_running_loop() + + except RuntimeError: + + _loop_stream_config = asyncio.new_event_loop() + + context.stream_config = _loop_stream_config.run_until_complete( context.route_bridge.downgrade_graph_to_stream( RouteConfig.from_graph_config( GraphConfig( diff --git a/src/cleveragents/agents/base.py b/src/cleveragents/agents/base.py index 69d7a5a2a..e4bb167b9 100644 --- a/src/cleveragents/agents/base.py +++ b/src/cleveragents/agents/base.py @@ -54,9 +54,17 @@ class Agent(ABC): def process_message_sync( self, message: Any, context: dict[str, Any] | None = None ) -> Any: - return asyncio.get_event_loop().run_until_complete( - self.process_message(message, context or {}) - ) + try: + _loop = asyncio.get_running_loop() + except RuntimeError: + _loop = asyncio.new_event_loop() + try: + return _loop.run_until_complete( + self.process_message(message, context or {}) + ) + finally: + if not _loop.is_running(): + _loop.close() @abstractmethod async def process_message( diff --git a/src/cleveragents/langgraph/bridge.py b/src/cleveragents/langgraph/bridge.py index 0e171e583..7aeb8df6b 100644 --- a/src/cleveragents/langgraph/bridge.py +++ b/src/cleveragents/langgraph/bridge.py @@ -224,7 +224,10 @@ class RxPyLangGraphBridge: ) def create_future_task(msg: StreamMessage) -> asyncio.Future[StreamMessage]: - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() task: asyncio.Future[StreamMessage] = asyncio.ensure_future( execute_graph(msg), loop=loop ) diff --git a/src/cleveragents/langgraph/nodes.py b/src/cleveragents/langgraph/nodes.py index c2cca9f24..1c5d19165 100644 --- a/src/cleveragents/langgraph/nodes.py +++ b/src/cleveragents/langgraph/nodes.py @@ -106,7 +106,10 @@ class Node: # pylint: disable=too-many-instance-attributes async def execute(self, state: GraphState) -> dict[str, Any]: # pylint: disable=too-many-branches self.execution_count += 1 - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() start_time = loop.time() try: state_updates = {"current_node": self.name} @@ -271,7 +274,10 @@ class Node: # pylint: disable=too-many-instance-attributes if asyncio.iscoroutinefunction(fn): result = await fn(state) else: - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() result = await loop.run_in_executor(None, lambda: fn(state)) if asyncio.iscoroutine(result): result = await result diff --git a/src/cleveragents/reactive/route_bridge.py b/src/cleveragents/reactive/route_bridge.py index b131c67ed..01336ae02 100644 --- a/src/cleveragents/reactive/route_bridge.py +++ b/src/cleveragents/reactive/route_bridge.py @@ -2,8 +2,8 @@ from __future__ import annotations -import asyncio import logging +import time from typing import Any from rx.scheduler.eventloop import AsyncIOScheduler # type: ignore[attr-defined] @@ -86,7 +86,7 @@ class RouteBridge: last_updated = state.metadata.get("last_updated") if ( last_updated - and (asyncio.get_event_loop().time() - last_updated) > idle_threshold + and (time.time() - last_updated) > idle_threshold ): return True if (