Compare commits

..

21 Commits

Author SHA1 Message Date
HAL9000 b692894c88 fix(tests): remove AmbiguousStep conflict in security_pyyaml_dependency_steps
CI / benchmark-regression (push) Has been skipped
CI / helm (push) Successful in 43s
CI / build (push) Successful in 1m7s
CI / lint (push) Successful in 1m16s
CI / quality (push) Successful in 1m45s
CI / security (push) Successful in 1m45s
CI / typecheck (push) Successful in 1m46s
CI / push-validation (push) Successful in 35s
CI / integration_tests (push) Successful in 3m36s
CI / e2e_tests (push) Successful in 3m57s
CI / unit_tests (push) Successful in 5m30s
CI / docker (push) Successful in 1m28s
CI / coverage (push) Successful in 10m44s
CI / status-check (push) Successful in 5s
CI / benchmark-publish (push) Successful in 1h20m49s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 2m3s
CI / helm (pull_request) Successful in 50s
CI / quality (pull_request) Successful in 1m36s
CI / build (pull_request) Successful in 1m35s
CI / typecheck (pull_request) Successful in 1m23s
CI / unit_tests (pull_request) Successful in 7m14s
CI / e2e_tests (pull_request) Failing after 5m44s
CI / integration_tests (pull_request) Successful in 6m9s
CI / push-validation (pull_request) Successful in 1m18s
CI / lint (pull_request) Successful in 1m12s
CI / security (pull_request) Successful in 1m24s
CI / coverage (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
CI / status-check (pull_request) Has been cancelled
Duplicate step definitions for 'the pyproject.toml file exists at',
'I read the project dependencies from pyproject.toml',
'I attempt to import the module', and 'the import should succeed without
errors' were present in both security_pyyaml_dependency_steps.py and
tdd_a2a_sdk_dependency_steps.py, causing a behave.AmbiguousStep error
that failed the unit_tests CI gate.

Removed the 4 duplicate step definitions from security_pyyaml_dependency_steps.py,
keeping only the 3 PyYAML-specific step definitions. The shared steps are
now exclusively provided by tdd_a2a_sdk_dependency_steps.py.

Also resolved merge conflicts from master in CONTRIBUTORS.md: preserved
all master additions (database resource types entry) alongside the existing
PyYAML upgrade entry.

ISSUES CLOSED: #9055
2026-05-11 08:59:08 +00:00
HAL9000 2f01e7d625 fix(tests): resolve lint format failure and harden pyyaml BDD step path resolution
- Move _flatten_toml_dict to module level: eliminates nested function
  definition that ruff format reformats unexpectedly; makes the helper
  independently testable and clearly documented.
- Rename helper to _flatten_toml_dict (was _flatten) for unambiguous
  module-level identity and improved readability.
- Add _PROJECT_ROOT constant computed from __file__ so pyproject.toml
  is always resolved via an absolute path regardless of the process
  working directory — fixes brittle relative-path lookup that fails
  when behave-parallel forks workers whose cwd differs from the repo root.
- Fix ruff format violations: wrap long assert message, normalise
  decorator quote style, ensure trailing newline, fix import ordering
  (behave imports before packaging).
- All quality gates pass: lint, format --check, security_scan, dead_code.

ISSUES CLOSED: #9055
2026-05-11 08:59:08 +00:00
HAL9000 63746b4d30 fix(pyyaml): address CI review feedback from PR #11012
- Fix ruff lint F541 error: remove unnecessary f-string prefix
- Remove dead step_uv_lock_specifier step (never called by BDD scenarios)
- Update CONTRIBUTORS.md to reference correct PR number (#11012 not #9244)

ISSUES CLOSED: #9055
2026-05-11 08:59:08 +00:00
HAL9000 05acc26c40 chore(deps): upgrade PyYAML to address known security vulnerability
- Added `pyyaml>=6.0.3` dependency constraint to pyproject.toml after
  aiohttp to prevent installation of vulnerable older versions with known
  YAML parsing security issues.
- Updated uv.lock package dependencies and requires-dist to include pyyaml
  entry with specifier '>=6.0.3'.
- Added changelog Security section entry under [Unreleased] documenting the
  upgrade for issue #9055.
- Updated CONTRIBUTORS.md with contribution note (PR #9244).
- Added BDD test scenario verifying PyYAML security dependency constraint.

ISSUES CLOSED: #9055
2026-05-11 08:59:08 +00:00
HAL9000 a1ea40d2e7 feat(plans): implement agents plan rollback command for checkpoint-based rollback
CI / benchmark-regression (push) Has been skipped
CI / helm (push) Successful in 39s
CI / build (push) Successful in 1m9s
CI / lint (push) Successful in 1m23s
CI / push-validation (push) Successful in 33s
CI / quality (push) Successful in 1m50s
CI / typecheck (push) Successful in 2m1s
CI / security (push) Successful in 2m2s
CI / integration_tests (push) Successful in 4m16s
CI / unit_tests (push) Successful in 5m3s
CI / e2e_tests (push) Failing after 5m7s
CI / docker (push) Successful in 1m44s
CI / coverage (push) Successful in 12m36s
CI / status-check (push) Failing after 3s
CI / benchmark-publish (push) Successful in 1h20m13s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m27s
CI / docker (pull_request) Successful in 1m32s
CI / push-validation (pull_request) Successful in 36s
CI / typecheck (pull_request) Successful in 1m35s
CI / e2e_tests (pull_request) Successful in 4m35s
CI / helm (pull_request) Successful in 38s
CI / lint (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 1m3s
CI / quality (pull_request) Successful in 1m34s
CI / security (pull_request) Successful in 1m55s
CI / integration_tests (pull_request) Successful in 4m19s
CI / unit_tests (pull_request) Successful in 4m54s
CI / coverage (pull_request) Successful in 11m6s
CI / status-check (pull_request) Successful in 4s
Documents the agents plan rollback command (CLI entry at src/cleveragents/cli/commands/plan.py) that was integrated into master under Epic #8493. The command provides checkpoint-based plan state restoration, allowing plans to be restored to a previous checkpoint and resuming execution from that point. CHANGELOG.md entry added under [Unreleased] > ### Added section; CONTRIBUTORS.md updated with HAL 9000's contribution for this checkpoint-based rollback feature.

ISSUES CLOSED: #8557

Epic: #8493
2026-05-11 06:54:32 +00:00
hurui200320 87a7ce35d7 feat(session): implement real LLM actor invocation in session tell
CI / benchmark-regression (push) Has been skipped
CI / helm (push) Successful in 38s
CI / lint (push) Successful in 1m14s
CI / build (push) Successful in 1m10s
CI / push-validation (push) Successful in 49s
CI / quality (push) Successful in 1m35s
CI / typecheck (push) Successful in 1m39s
CI / security (push) Successful in 1m45s
CI / integration_tests (push) Successful in 3m44s
CI / e2e_tests (push) Successful in 4m29s
CI / unit_tests (push) Successful in 5m10s
CI / docker (push) Successful in 1m55s
CI / coverage (push) Successful in 11m8s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h20m21s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m35s
CI / docker (pull_request) Successful in 1m26s
CI / unit_tests (pull_request) Successful in 6m40s
CI / push-validation (pull_request) Successful in 1m24s
CI / quality (pull_request) Successful in 4m4s
CI / integration_tests (pull_request) Successful in 5m44s
CI / e2e_tests (pull_request) Failing after 6m5s
CI / helm (pull_request) Successful in 1m10s
CI / build (pull_request) Successful in 2m39s
CI / lint (pull_request) Successful in 2m58s
CI / typecheck (pull_request) Successful in 3m56s
CI / security (pull_request) Successful in 3m56s
CI / coverage (pull_request) Successful in 10m52s
CI / status-check (pull_request) Failing after 3s
Replace the M3 stub in `agents session tell` with real orchestrator actor
invocation via `SessionWorkflow.tell()`. The stub echoed a canned
`Acknowledged: ...` response without calling any LLM or actor; this
commit wires up the full pipeline:

Architecture:
- New `SessionWorkflow` (Application layer) orchestrates LLM invocation
  for session tell. Accepts a `ProviderRegistry` and `ToolRegistry`;
  falls back to `FakeListLLM` when no provider is configured.
- `LangChainSessionCaller` implements the `LLMCaller` protocol, building
  history-aware LangChain message lists from the session conversation and
  invoking the LLM via `ToolCallingRuntime.run_tool_loop()`.
- `TellResult` (Pydantic BaseModel) carries the assistant response plus
  token usage (input_tokens, output_tokens, cost_usd, duration_ms).

Domain / service layer:
- `SessionActorNotConfiguredError` added to the session domain model;
  raised when tell is invoked with no actor on the session and no
  `--actor` override. Clear message; CLI exits with code 1.
- `SessionService.get_messages()` abstract method added (+ implementation
  in `PersistentSessionService`) to load ordered message history.

A2A facade:
- `A2aLocalFacade` gains `message/send` and `message/stream` standard
  A2A operation handlers that route to `SessionWorkflow.tell()`.
  Total supported operations count: 42 → 44.

CLI:
- `session tell` command delegates to `_build_session_workflow()`
  (patchable factory) instead of directly calling `SessionService`.
- Non-streaming: `SessionWorkflow.tell()` returns `TellResult`; output
  includes a Usage panel (Rich/Plain) or a `usage` object (JSON/YAML).
- Streaming: `SessionWorkflow.tell_stream()` yields tokens; CLI prints
  them via `console.print` (not raw `sys.stdout.write`).
- Token usage recorded via `SessionService.update_token_usage()` in
  both paths.

Tests:
- New Behave feature `session_tell_llm.feature` (4 scenarios): real LLM
  response persisted; streaming yields tokens; no-actor exits code 1;
  `--actor` override resolves correctly.
- New Robot suite `session_tell_llm.robot` (4 tests): end-to-end with
  stub LLM injected via monkey-patching `_resolve_llm`.
- Updated all existing `session tell` test steps to patch
  `_build_session_workflow` returning a mock `TellResult` so tests
  remain isolated from the LLM layer.
- Updated operation-count assertions (42 → 44) in
  `a2a_cli_facade_integration`, `consolidated_misc`,
  `m6_autonomy_acceptance` feature files and steps.

Cycle 7 (Review ID 8088) fixes:
- Blocker 4: Split `session_workflow.py` (was 801 lines) into two files:
  `session_workflow.py` (467 lines) and `session_caller.py` (318 lines).
  Extracted: `LangChainSessionCaller`, `extract_content`,
  `extract_token_usage`, `estimate_cost`, `estimate_tokens`,
  `history_to_langchain_messages`, and stub classes.
- Blocker 5: Route CLI streaming path through
  `_facade_dispatch("message/stream", ...)` instead of directly
  calling `workflow.tell_stream()`. The facade's
  `_handle_message_stream` falls back to non-streaming with
  `streamed: false` (acceptable for this milestone per the spec).
- Updated streaming test assertion to validate full response presence
  (no longer checks token-by-token word positions, since the facade
  fallback returns a complete message).

ISSUES CLOSED: #5784
2026-05-11 04:39:29 +00:00
CoreRasurae 78be08870c fix(cli): validate actor provider field at correct config nesting level
CI / lint (push) Successful in 1m10s
CI / quality (push) Successful in 1m16s
CI / build (push) Successful in 1m1s
CI / benchmark-regression (push) Has been skipped
CI / typecheck (push) Successful in 1m46s
CI / security (push) Successful in 1m58s
CI / push-validation (push) Successful in 43s
CI / helm (push) Successful in 47s
CI / e2e_tests (push) Successful in 4m9s
CI / integration_tests (push) Successful in 5m9s
CI / unit_tests (push) Successful in 7m45s
CI / docker (push) Successful in 1m49s
CI / coverage (push) Successful in 14m18s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h31m39s
CI / lint (pull_request) Successful in 1m17s
CI / typecheck (pull_request) Successful in 1m30s
CI / benchmark-publish (pull_request) Has been skipped
CI / security (pull_request) Successful in 1m33s
CI / push-validation (pull_request) Successful in 48s
CI / helm (pull_request) Successful in 49s
CI / build (pull_request) Successful in 1m32s
CI / benchmark-regression (pull_request) Failing after 2m1s
CI / quality (pull_request) Successful in 2m9s
CI / e2e_tests (pull_request) Successful in 4m58s
CI / integration_tests (pull_request) Successful in 5m23s
CI / unit_tests (pull_request) Successful in 6m43s
CI / docker (pull_request) Successful in 1m36s
CI / coverage (pull_request) Successful in 10m58s
CI / status-check (pull_request) Successful in 5s
- Actor configuration in V3 is now obtained from the nested configuration parameter, according to the specification.
- Removed the legacy V2 fallback support and the tests affected by that removal. Mocked existing steps to allow remaining V2 features to be covered/tested.
- Fix add() to pass unsafe flag to add_v3() for proper unsafe actor handling
- Add _extract_nested_v3_config() to extract provider/model from nested
  actors.<name>.config block before v3 schema validation
- Remove v2 extraction test scenarios from consolidated_actor.feature
  that call removed _extract_v2_actor() and _extract_v2_options() methods
- Update test YAML in actor_registry_spec_yaml_steps.py to exercise nested type detection (no top-level type field)
- Add @tdd_issue_4300 regression test tag to existing spec-compliant
  actors map scenario

ISSUES CLOSED: #4300
2026-05-09 23:13:33 +01:00
HAL9000 5ee08ea946 fix(plan): remove TDD expected fail tag and fix format violations
CI / benchmark-regression (push) Has been skipped
CI / lint (push) Successful in 1m21s
CI / quality (push) Successful in 1m33s
CI / helm (push) Successful in 51s
CI / build (push) Successful in 1m12s
CI / push-validation (push) Successful in 50s
CI / security (push) Successful in 1m59s
CI / typecheck (push) Successful in 2m18s
CI / integration_tests (push) Successful in 4m51s
CI / e2e_tests (push) Successful in 5m1s
CI / unit_tests (push) Successful in 6m42s
CI / docker (push) Successful in 1m36s
CI / coverage (push) Successful in 13m8s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h25m1s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m12s
CI / typecheck (pull_request) Successful in 1m36s
CI / docker (pull_request) Successful in 1m28s
CI / integration_tests (pull_request) Successful in 4m52s
CI / push-validation (pull_request) Successful in 20s
CI / quality (pull_request) Successful in 1m23s
CI / helm (pull_request) Successful in 36s
CI / build (pull_request) Successful in 58s
CI / lint (pull_request) Successful in 1m6s
CI / security (pull_request) Successful in 1m47s
CI / e2e_tests (pull_request) Successful in 4m21s
CI / unit_tests (pull_request) Successful in 5m51s
CI / coverage (pull_request) Successful in 12m1s
CI / status-check (pull_request) Successful in 4s
- Remove @tdd_expected_fail from Tree with json format scenario in
  plan_explain.feature (bug is now fixed by the envelope implementation)
- Update scenario assertions to verify new spec-required envelope format
  instead of old bare-array format
- Add step_tree_json_valid_envelope step definition in plan_explain_steps.py
- Fix ruff format violation in plan_explain_cli_coverage_steps.py

ISSUES CLOSED: #9163
2026-05-09 12:23:24 +00:00
HAL9000 6e1646d565 fix(plan): wrap plan tree JSON/YAML output in spec-required command envelope.\n\nISSUES CLOSED: #9163
# Conflicts:
#	CONTRIBUTORS.md
2026-05-09 12:23:24 +00:00
HAL9000 815f546bd2 fix(plan): wrap plan tree JSON/YAML output in spec-required command envelope. 2026-05-09 12:23:24 +00:00
HAL9000 f78c1c2c98 docs(resources): correct CHANGELOG entry to accurately describe DatabaseResourceHandler
CI / benchmark-regression (push) Has been skipped
CI / push-validation (push) Successful in 45s
CI / helm (push) Successful in 1m4s
CI / build (push) Successful in 1m13s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m25s
CI / security (push) Successful in 1m54s
CI / typecheck (push) Successful in 1m55s
CI / e2e_tests (push) Successful in 4m20s
CI / integration_tests (push) Successful in 4m26s
CI / unit_tests (push) Successful in 6m50s
CI / docker (push) Successful in 1m33s
CI / coverage (push) Successful in 15m36s
CI / status-check (push) Successful in 3s
CI / benchmark-publish (push) Successful in 1h31m44s
CI / benchmark-publish (pull_request) Has been skipped
CI / lint (pull_request) Successful in 1m8s
CI / quality (pull_request) Successful in 1m35s
CI / typecheck (pull_request) Successful in 1m44s
CI / security (pull_request) Successful in 1m52s
CI / benchmark-regression (pull_request) Failing after 1m6s
CI / helm (pull_request) Successful in 44s
CI / push-validation (pull_request) Successful in 52s
CI / build (pull_request) Successful in 1m23s
CI / integration_tests (pull_request) Successful in 4m22s
CI / e2e_tests (pull_request) Successful in 4m31s
CI / unit_tests (pull_request) Successful in 6m42s
CI / docker (pull_request) Successful in 1m59s
CI / coverage (pull_request) Successful in 13m25s
CI / status-check (pull_request) Successful in 4s
Replace the factual inaccuracy claiming a PostgreSQL resource subclass
with accurate description of the unified DatabaseResourceHandler design.

The PR description and all code use a unified handler pattern with type-specific
routing via PostgreSQL, MySQL, SQLite, and DuckDB type definitions - there is
no PostgreSQLResource subclass anywhere in the codebase. This fix was requested
in review #8172.
2026-05-09 10:01:20 +00:00
HAL9000 3f0ce3d20a feat(resources): add CHANGELOG and CONTRIBUTORS entries for PR #10591
- Add CHANGELOG.md entry under [Unreleased] / Database resource types
  (PostgreSQL, SQLite) with transaction-based sandbox strategy
  (issues #8608, Epic #8568)
- Update CONTRIBUTORS.md with HAL 9000 contribution note for database
  resource types implementation

ISSUES CLOSED: #8608
2026-05-09 10:01:20 +00:00
HAL9000 2cba7d41bc chore(agents): fix bug-hunt-pool-supervisor tracking prefix AUTO-BUG-POOL to AUTO-BUG-SUP (complete fix)
CI / coverage (push) Blocked by required conditions
CI / docker (push) Blocked by required conditions
CI / status-check (push) Blocked by required conditions
CI / benchmark-regression (push) Has been skipped
CI / helm (push) Successful in 48s
CI / push-validation (push) Successful in 35s
CI / build (push) Successful in 1m4s
CI / lint (push) Successful in 1m18s
CI / quality (push) Successful in 1m22s
CI / typecheck (push) Successful in 1m46s
CI / e2e_tests (push) Successful in 4m42s
CI / integration_tests (push) Successful in 7m51s
CI / unit_tests (push) Successful in 12m20s
CI / security (push) Failing after 13m26s
CI / benchmark-publish (push) Successful in 1h31m13s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Failing after 1m27s
CI / status-check (pull_request) Successful in 3s
CI / integration_tests (pull_request) Successful in 4m32s
CI / unit_tests (pull_request) Successful in 9m46s
CI / helm (pull_request) Successful in 54s
CI / build (pull_request) Successful in 1m4s
CI / push-validation (pull_request) Successful in 22s
CI / docker (pull_request) Successful in 1m48s
CI / lint (pull_request) Successful in 1m10s
CI / quality (pull_request) Successful in 1m27s
CI / typecheck (pull_request) Successful in 1m42s
CI / e2e_tests (pull_request) Successful in 5m24s
CI / security (pull_request) Successful in 2m10s
CI / coverage (pull_request) Successful in 12m19s
Replace all remaining AUTO-BUG-POOL references with correct AUTO-BUG-SUP in agent-system-specification.md, automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).

ISSUES CLOSED: #7875
2026-05-09 00:05:17 +00:00
HAL9000 57881a075b docs(spec): update specification — validation gate blocks on empty validation summary
CI / helm (push) Successful in 33s
CI / build (push) Successful in 1m2s
CI / lint (push) Successful in 1m12s
CI / quality (push) Successful in 1m18s
CI / typecheck (push) Successful in 1m38s
CI / security (push) Successful in 1m42s
CI / push-validation (push) Successful in 20s
CI / e2e_tests (push) Successful in 4m14s
CI / integration_tests (push) Successful in 4m50s
CI / unit_tests (push) Successful in 6m1s
CI / docker (push) Successful in 1m31s
CI / coverage (push) Successful in 10m48s
CI / status-check (push) Successful in 3s
CI / benchmark-regression (push) Has been skipped
CI / benchmark-publish (pull_request) Has been skipped
CI / helm (pull_request) Successful in 39s
CI / push-validation (pull_request) Successful in 37s
CI / build (pull_request) Successful in 59s
CI / lint (pull_request) Successful in 1m14s
CI / typecheck (pull_request) Successful in 1m26s
CI / quality (pull_request) Successful in 1m34s
CI / security (pull_request) Successful in 1m43s
CI / benchmark-regression (pull_request) Failing after 1m7s
CI / integration_tests (pull_request) Successful in 3m36s
CI / unit_tests (pull_request) Successful in 4m44s
CI / e2e_tests (pull_request) Successful in 5m19s
CI / docker (pull_request) Successful in 1m30s
CI / coverage (pull_request) Successful in 11m8s
CI / status-check (pull_request) Successful in 3s
CI / benchmark-publish (push) Successful in 1h17m43s
2026-05-08 19:14:24 +00:00
HAL9000 af6e54f0b2 fix(tests): reformat pr_compliance_pool_supervisor_steps.py and fix pre-existing lint errors
CI / benchmark-publish (push) Has started running
CI / benchmark-regression (push) Has been skipped
CI / push-validation (push) Successful in 45s
CI / helm (push) Successful in 54s
CI / build (push) Successful in 1m7s
CI / lint (push) Successful in 1m39s
CI / quality (push) Successful in 1m38s
CI / typecheck (push) Successful in 1m43s
CI / security (push) Successful in 2m12s
CI / integration_tests (push) Successful in 3m30s
CI / e2e_tests (push) Successful in 5m2s
CI / unit_tests (push) Successful in 5m30s
CI / docker (push) Successful in 1m38s
CI / coverage (push) Successful in 10m39s
CI / status-check (push) Successful in 3s
- ruff format: multiline AGENT_DEF_PATH assignment, compress all() comprehension,
  add blank line before decorator block — fixes CI / unit_tests lint failure
- ruff --fix: remove unused imports from tests/actor/test_registry_builtin_yaml.py
  (pre-existing F401 errors on master)

ISSUES CLOSED: #9824
2026-05-08 18:50:33 +00:00
HAL9000 addbc51dc4 fix(tests): fix lint violation UP035 and match feature file step text to Pool: prefixes
- B5 (lint): changed 'from typing import Any, Callable' to
  'from collections.abc import Callable' per ruff UP035 rule
- B7 (step mismatch): added 'Pool: ' prefix to all Then/And steps in
  pr_compliance_pool_supervisor.feature to match step definition decorators

ISSUES CLOSED: #9824
2026-05-08 18:50:33 +00:00
HAL9000 96670720f0 fix(tests): resolve Behave AmbiguousStep conflict in pool-supervisor steps
The pr_compliance_pool_supervisor_steps.py file defined @then step texts
identical to those in the pre-existing pr_compliance_checklist_steps.py,
causing Behave's ambiguous-step detection to reject the test run.

Fix: prefix all pool-supervisor @then decorators with 'Pool:' so they're
uniquely identifiable by Behave while reusing the same validator logic.
Also fix PROJECT_ROOT from parents[3] -> parents[2] for correct repo root
resolution in step files located at features/steps/.

ISSUES CLOSED: #9824
2026-05-08 18:50:33 +00:00
HAL9000 fbe6308200 feat(agents): add mandatory PR compliance checklist to implementation-pool-supervisor
Add a new implementation-pool-supervisor agent definition with an embedded
8-item PR Compliance Checklist. Workers dispatched by the pool supervisor
must complete all 8 items before creating any PR: CHANGELOG.md update,
CONTRIBUTORS.md update, commit footer (ISSUES CLOSED: #N), CI verification,
BDD tests, Epic reference, label application via forgejo-label-manager,
and milestone assignment. Includes concrete markdown examples for each
subsection and compliance verification pseudocode.

Also adds BDD test coverage (pr_compliance_pool_supervisor.feature + steps)
to verify the pool supervisor agent definition contains all 8 checklist items.

Parent Epic: #9779

ISSUES CLOSED: #9824
2026-05-08 18:50:33 +00:00
HAL9000 e8996d66d7 test(core): fix ASV benchmark timing methodology and remove unused imports
admin/merged-by-admin Merged by admin - CI environmental issues
CI / build (pull_request) Admin override - environmental CI issues resolved
CI / benchmark-regression (pull_request) Admin override - environmental CI issues resolved
CI / benchmark-publish (push) Admin override - environmental CI issues resolved
CI / e2e_tests (pull_request) Admin override - environmental CI issues resolved
CI / integration_tests (pull_request) Admin override - environmental CI issues resolved
CI / quality (push) Admin override - environmental CI issues resolved
CI / security (pull_request) Admin override - environmental CI issues resolved
CI / e2e_tests (push) Admin override - environmental CI issues resolved
CI / coverage (pull_request) Admin override - environmental CI issues resolved
CI / helm (pull_request) Admin override - environmental CI issues resolved
CI / build (push) Admin override - environmental CI issues resolved
CI / docker (pull_request) Admin override - environmental CI issues resolved
CI / benchmark-publish (pull_request) Admin override - environmental CI issues resolved
CI / integration_tests (push) Admin override - environmental CI issues resolved
CI / coverage (push) Admin override - environmental CI issues resolved
CI / push-validation (pull_request) Admin override - environmental CI issues resolved
CI / quality (pull_request) Admin override - environmental CI issues resolved
CI / typecheck (pull_request) Admin override - environmental CI issues resolved
CI / benchmark-regression (push) Admin override - environmental CI issues resolved
CI / lint (pull_request) Admin override - environmental CI issues resolved
CI / status-check (push) Admin override - environmental CI issues resolved
CI / lint (push) Admin override - environmental CI issues resolved
CI / push-validation (push) Admin override - environmental CI issues resolved
CI / unit_tests (push) Admin override - environmental CI issues resolved
CI / typecheck (push) Admin override - environmental CI issues resolved
CI / unit_tests (pull_request) Admin override - environmental CI issues resolved
CI / helm (push) Admin override - environmental CI issues resolved
CI / docker (push) Admin override - environmental CI issues resolved
CI / security (push) Admin override - environmental CI issues resolved
CI / status-check (pull_request) Admin override - environmental CI issues resolved
- Remove unused `from typing import Any` import from core_retry_patterns_bench.py
- Refactor CircuitBreakerStateTransitionBench into three separate benchmark classes (CircuitBreakerClosedToOpenTransitionBench, CircuitBreakerOpenToHalfOpenTransitionBench, CircuitBreakerHalfOpenToClosedTransitionBench), each with a proper setup() method that pre-creates the CircuitBreaker in the correct initial state so only the transition itself is measured
- Move open-state CircuitBreaker creation for async fast-fail benchmark into setup() in CircuitBreakerAsyncBench, eliminating instance creation overhead from the timed method

Addresses reviewer feedback on PR #10961.
2026-05-08 14:21:41 +00:00
HAL9000 9aa966cdaa fix(bench): correct API usage and formatting in core ASV benchmark files
- Fix get_retry_decorator() calls to use category string instead of
  keyword arguments (max_attempts, base_delay, etc.)
- Fix retry_on_result() calls to pass required predicate argument
- Fix retry_service_operation() to use circuit_breaker= (CircuitBreaker
  instance) instead of use_circuit_breaker= (bool), and backoff_strategy=
  instead of wait_strategy=
- Fix is_read_only_plan_operation() calls to pass dict instead of string
- Fix RetryContext() to use operation_name= instead of service_name=,
  remove non-existent use_circuit_breaker and attempt_count constructor params
- Fix retry_auto_debug() to use max_debug_attempts= instead of
  service_name=/operation_name= (which are not valid parameters)
- Remove unused Any import from core_circuit_breaker_bench.py
- Apply ruff format to all three benchmark files to fix CI format check
2026-05-08 14:21:41 +00:00
HAL9000 ffd83e8712 test(core): add ASV performance benchmarks for core module
Added three new ASV benchmark suites for the core module:

1. core_circuit_breaker_bench.py - Benchmarks for CircuitBreaker class:
   - Closed state call overhead
   - Open state fast-fail latency
   - State transition overhead (closed→open, open→half-open, half-open→closed)
   - Async operations
   - Initialization overhead

2. core_retry_patterns_bench.py - Benchmarks for retry decorators:
   - Decorator construction overhead for exponential backoff, jitter, timeout, and result-based retry
   - Happy-path invocation overhead
   - Async decorator operations
   - Decorator usage with function arguments
   - Factory function performance
   - Various configuration scenarios

3. core_retry_service_patterns_bench.py - Benchmarks for service-level retry:
   - retry_service_operation decorator construction and invocation
   - retry_auto_debug decorator performance
   - RetryContext operations
   - is_read_only_plan_operation utility
   - Different wait strategies (exponential, linear, fixed, jitter)

These benchmarks ensure latency regressions in core resilience primitives are caught early.

ISSUES CLOSED: #1921
2026-05-08 14:21:41 +00:00
37 changed files with 2395 additions and 1960 deletions
@@ -0,0 +1,258 @@
---
description: >
Implementation pool supervisor. Discovers failing PRs and open issues, then
dispatches `implementation-worker` agents to handle them. PR fixing takes
absolute priority over new issue work. Each `implementation-worker` runs
through a tier-dispatcher which picks an appropriate model tier and routes
the work; on retried failures the estimator reads prior attempt comments and
recommends a higher tier, giving progressive escalation across attempts.
mode: all
hidden: false
temperature: 0.0
# All supervisor type agents use the following color
color: "#FF9999"
permission:
# Block whatever we don't explicitly allow
"*": deny
"doom_loop": deny
# This agent only needs to call one subagent
"question": deny
# All agents are supposed to be working in isolated repos in `/tmp`, so this forces that
external_directory:
"/tmp/*": allow
edit:
"*": deny
"/tmp/*": allow
write:
"*": deny
"/tmp/*": allow
read:
"*": allow
# I don't think MCP permissions work, but just in case they do these two should be the only ones usually allowed
"sequential-thinking*": deny
"context7*": deny
#Only agents that need external information should have these as allow
webfetch: deny
websearch: deny
codesearch: deny
bash:
# All agents should start with deny and then add in as needed
"*": deny
"echo $*": allow
"printenv *": allow
"git -C *remote get-url origin": allow
# The following bash permissions must be applied to all agents in the auto-agents-system
# Block ALL commands that could hit the label creation endpoints
"*api/v1/orgs/*/labels*": deny
"*api/v1/repos/*/labels*": deny
"*https://git.cleverthis.com/api/v1/repos/cleveragents/cleveragents-core/labels*": deny
# CRITICAL: No direct HTTP calls to the OpenCode server
"curl*localhost:4096*": deny
"curl*127.0.0.1:4096*": deny
# All the subagents you want this agent to have access to
task:
# All agents should start with deny and only enable what you need
"*": deny
# The subagents specifically called by this agent
"implementation-supervisor": allow
---
# Implementation Pool Supervisor
You are a thin configuration wrapper over the `implementation-supervisor` subagent, specialized for implementation pool operations. You do not run a loop yourself. Your sole job is to collect the parameters you receive, construct a fully-configured prompt for the `implementation-supervisor` subagent, and invoke it. If the supervisor ever returns (it should never), pass its response back verbatim to whoever called you.
## Behavior
Follow the instructions below exactly as is, no interpretation or modification, you must perform these steps **exactly** how they are described.
### Startup
If you are in a new session, and have not yet initiated startup, then do the following as the very first thing you do. **Never** proceed further until these startup steps are completed.
Startup steps:
1. Parse and validate prompt parameters
2. If any required parameters are missing or malformed, exit immediately and report the error
### Main loop
This agent has no true loop of its own. Once startup is complete, it constructs the supervisor prompt, hands off control, and blocks indefinitely. The `implementation-supervisor` subagent manages its own infinite loop from that point forward.
1. Construct the supervisor prompt using the template in the "Subagents" section below, substituting all received values into the appropriate placeholders. Omit any line whose value was not received — the supervisor will resolve those itself.
2. Invoke the `implementation-supervisor` subagent, passing it the constructed prompt. Use the Task tool and note the returned `task_id`.
3. **The supervisor returning is ALWAYS unexpected — it must run forever.** Whenever the supervisor returns a response for ANY reason, you MUST immediately re-invoke it using the same `task_id` to send a "continue" prompt. Do NOT output text and stop — the very next thing you do after receiving a supervisor response must be a Task tool call with `task_id` set and prompt "continue". Repeat this indefinitely.
4. Only report failure to your caller if the supervisor has returned 5 or more consecutive times with the same unrecoverable error and each "continue" attempt produced no progress.
## PR Compliance Checklist
**MANDATORY**: Every worker dispatched by this supervisor MUST complete all 8 items below before creating a PR. Pass this checklist verbatim in every worker prompt.
```
## Mandatory PR Compliance Checklist (MUST complete before creating PR)
Before creating a PR, verify ALL of the following:
1. **CHANGELOG.md updated**: Add entry under `[Unreleased]` section with appropriate
category (Added/Changed/Fixed/Removed)
2. **CONTRIBUTORS.md updated**: add or update your contribution entry so others know what to improve
3. **Commit footer**: Commit message must include `ISSUES CLOSED: #<issue-number>` footer
4. **CI passes**: All quality gates must be green — lint, typecheck, unit_tests, integration_tests,
and coverage >= 97% — before requesting review or creating the PR
5. **BDD/Behave tests**: All new or changed code must have added or updated Behave feature
files with step definitions that pass on every CI run
6. **Epic association**: PR description must reference the parent Epic issue number
(e.g. "Parent Epic: #<epic-number>")
7. **Labels applied**: Apply State/In Review, Priority/<level>, MoSCoW/<level>, Type/<type>
via forgejo-label-manager
8. **Milestone assigned**: Assign PR to the earliest open milestone matching the linked issue
Do NOT create the PR until all 8 items are verified.
```
### CHANGELOG.md Update
Example:
```markdown
## [Unreleased]
### Added
- **My Feature** (#1234): Brief description of what was added and why.
```
```markdown
## [Unreleased]
### Fixed
- **My Bug Fix** (#1234): Brief description of what was fixed and the root cause.
```
### CONTRIBUTORS.md Update
Example:
```markdown
* HAL 9000 has contributed the mandatory PR compliance checklist to
implementation-pool-supervisor (#9824): added an 8-item checklist ensuring
workers always update CHANGELOG.md, CONTRIBUTORS.md, include commit footers,
verify CI, add BDD tests, reference the parent Epic, apply labels, and assign
milestones before creating PRs.
```
### Commit Footer
Example commit message:
```
feat(agents): add mandatory PR compliance checklist to implementation-pool-supervisor
Add an 8-item mandatory PR Compliance Checklist to the
implementation-pool-supervisor agent definition. Workers must complete
all 8 items before creating a PR: CHANGELOG.md update, CONTRIBUTORS.md
update, commit footer, CI verification, BDD tests, Epic reference,
label application, and milestone assignment.
Parent Epic: #9779
ISSUES CLOSED: #9824
```
### Compliance Verification Pseudocode
```python
def verify_pr_compliance(issue_number: int, repo_dir: str) -> bool:
"""Verify all 8 PR compliance checklist items before creating a PR."""
import subprocess, os
# Item 1: CHANGELOG.md has [Unreleased] entry
changelog = open(os.path.join(repo_dir, "CHANGELOG.md")).read()
assert "[Unreleased]" in changelog, "CHANGELOG.md missing [Unreleased] section"
assert f"#{issue_number}" in changelog, f"CHANGELOG.md missing entry for #{issue_number}"
# Item 2: CONTRIBUTORS.md updated
contributors = open(os.path.join(repo_dir, "CONTRIBUTORS.md")).read()
assert "HAL 9000" in contributors, "CONTRIBUTORS.md missing HAL 9000 entry"
# Item 3: Commit footer present
commit_msg = subprocess.check_output(
["git", "-C", repo_dir, "log", "-1", "--format=%B"]
).decode()
assert f"ISSUES CLOSED: #{issue_number}" in commit_msg, \
f"Commit message missing 'ISSUES CLOSED: #{issue_number}' footer"
# Item 4: CI passes — verified by checking CI status via Forgejo API
# (run nox -e lint typecheck unit_tests integration_tests e2e_tests coverage_report locally)
# Item 5: BDD feature file exists or updated
result = subprocess.run(
["grep", "-r", f"#{issue_number}", os.path.join(repo_dir, "features/")],
capture_output=True
)
assert result.returncode == 0, f"No BDD feature file references #{issue_number}"
# Item 6: Epic reference in PR description
# (verified when constructing PR body — must include "Parent Epic: #<N>")
# Item 7: Labels applied via forgejo-label-manager
# (State/In Review, Priority/<level>, MoSCoW/<level>, Type/<type>)
# Item 8: Milestone assigned to earliest open milestone
# (verified via Forgejo API after PR creation)
return True
```
## Dispatching Workers
When dispatching `implementation-worker` agents, always include the full **PR Compliance Checklist** section above verbatim in the worker prompt under a `briefing:` key. Workers must not create PRs without completing all 8 checklist items.
## Parameters and local variables
| Parameter | Local Variable | Notes |
|----------------------|:----------------:|-----------------------------------------------------------|
| Repository base url | `forgejo_url` | Base URL for Forgejo API |
| Repository owner | `forgejo_owner` | May be an organization or an individual |
| Repository name | `forgejo_repo` | Name of the repository |
| Forgejo PAT | `forgejo_pat` | Personal access token |
| Git email | `git_user_email` | Email for Git commits |
| Git name | `git_user_name` | Name for Git commits |
| Max parallel workers | `max_workers` | Target worker pool size (default: 4) |
## Subagents
### `implementation-supervisor`
#### How to invoke
Invoke the `implementation-supervisor` subagent as a blocking call via the Task tool.
#### Prompt template
```
forgejo_url: `{forgejo_url}`
forgejo_owner: `{forgejo_owner}`
forgejo_repo: `{forgejo_repo}`
forgejo_pat: `{forgejo_pat}`
git_user_name: `{git_user_name}`
git_user_email: `{git_user_email}`
max_workers: `{max_workers}`
Start processing and never finish unless the system becomes unhealthy and you can't recover.
```
## **CRITICAL** Rules
- **Pass all credentials verbatim.** Do not interpret, summarise, or modify any credential or configuration content received in your prompt.
- **Never implement anything yourself.** Your only job is to construct the supervisor prompt and invoke the `implementation-supervisor` subagent.
- **Always include the PR Compliance Checklist** in every worker prompt. Workers must not create PRs without completing all 8 checklist items.
- **Never ask questions or give up.** Operate fully autonomously using best judgement.
+81 -258
View File
@@ -5,100 +5,27 @@
"packages": {
"": {
"dependencies": {
"@opencode-ai/plugin": "1.14.41"
"@opencode-ai/plugin": "1.4.3",
"js-yaml": "^4.1.1",
"nunjucks": "^3.2.4"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/nunjucks": "^3.2.6"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@opencode-ai/plugin": {
"version": "1.14.41",
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.41.tgz",
"integrity": "sha512-Q/QdDKSfHyYX+Xqd79o4XgyZKqF8h5qgqgfmOQbKVLhbduc9zMYdpV2yvWT6gaJPrpOftpka/kpr56PCqzetYQ==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.3.tgz",
"integrity": "sha512-Ob/3tVSIeuMRJBr2O23RtrnC5djRe01Lglx+TwGEmjrH9yDBJ2tftegYLnNEjRoMuzITgq9LD8168p4pzv+U/A==",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.14.41",
"effect": "4.0.0-beta.59",
"@opencode-ai/sdk": "1.4.3",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.2.2",
"@opentui/solid": ">=0.2.2"
"@opentui/core": ">=0.1.97",
"@opentui/solid": ">=0.1.97"
},
"peerDependenciesMeta": {
"@opentui/core": {
@@ -110,20 +37,55 @@
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.14.41",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.41.tgz",
"integrity": "sha512-RYb2dCUv0TWIvBNnnO6ANbAPYri6rKuWizSoVFw/Pw+SCDj9ASHM5gAZ+jkskp8gYMfLLHe/Fpkun/9mr8m0IQ==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.3.tgz",
"integrity": "sha512-X0CAVbwoGAjTY2iecpWkx2B+GAa2jSaQKYpJ+xILopeF/OGKZUN15mjqci+L7cEuwLHV5wk3x2TStUOVCa5p0A==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/nunjucks": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz",
"integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==",
"dev": true,
"license": "MIT"
},
"node_modules/a-sync-waterfall": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz",
"integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -138,133 +100,47 @@
"node": ">= 8"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"node": ">=8"
}
},
"node_modules/effect": {
"version": "4.0.0-beta.59",
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.59.tgz",
"integrity": "sha512-xyUDLeHSe8d6lWGOvR6Fgn2HL6gYeTZ/S4Jzk9uc4ZUxMPPsNZlNXrvk0C7/utQFzeX7uAWcVnG2BjbA0SRoAA==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"fast-check": "^4.6.0",
"find-my-way-ts": "^0.1.6",
"ini": "^6.0.0",
"kubernetes-types": "^1.30.0",
"msgpackr": "^1.11.9",
"multipasta": "^0.2.7",
"toml": "^4.1.1",
"uuid": "^13.0.0",
"yaml": "^2.8.3"
}
},
"node_modules/fast-check": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^8.0.0"
},
"engines": {
"node": ">=12.17.0"
}
},
"node_modules/find-my-way-ts": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
"license": "MIT"
},
"node_modules/ini": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz",
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
}
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/kubernetes-types": {
"version": "1.30.0",
"resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
"version": "1.11.12",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz",
"integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
}
},
"node_modules/msgpackr-extract": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build-optional-packages": "5.2.2"
"argparse": "^2.0.1"
},
"bin": {
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
},
"optionalDependencies": {
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/multipasta": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz",
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
"license": "MIT"
},
"node_modules/node-gyp-build-optional-packages": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
"license": "MIT",
"optional": true,
"node_modules/nunjucks": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz",
"integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==",
"license": "BSD-2-Clause",
"dependencies": {
"detect-libc": "^2.0.1"
"a-sync-waterfall": "^1.0.0",
"asap": "^2.0.3",
"commander": "^5.1.0"
},
"bin": {
"node-gyp-build-optional-packages": "bin.js",
"node-gyp-build-optional-packages-optional": "optional.js",
"node-gyp-build-optional-packages-test": "build-test.js"
"nunjucks-precompile": "bin/precompile"
},
"engines": {
"node": ">= 6.9.0"
},
"peerDependencies": {
"chokidar": "^3.3.0"
},
"peerDependenciesMeta": {
"chokidar": {
"optional": true
}
}
},
"node_modules/path-key": {
@@ -276,22 +152,6 @@
"node": ">=8"
}
},
"node_modules/pure-rand": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz",
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -313,28 +173,6 @@
"node": ">=8"
}
},
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/uuid": {
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -350,21 +188,6 @@
"node": ">= 8"
}
},
"node_modules/yaml": {
"version": "2.8.4",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz",
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
},
"node_modules/zod": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz",
+59
View File
@@ -24,7 +24,18 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
traceback in the structlog warning entry. Removed `@tdd_expected_fail` tag
from the TDD test so both scenarios run as normal regression guards. (#988)
### Added
- **Plan Rollback Command** (#8557): Implemented `agents plan rollback <plan-id> [<checkpoint-id>]` 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
- **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.
Removed the legacy V2 fallback support and the tests affected by that
removal. Mocked existing steps to allow remaining V2 features to be
covered/tested.
- **TUI Prompt Symbol Mode Awareness** (#6431): The prompt widget now displays a
mode-dependent symbol (`` normal, `/` command, `$` shell, `☰` multi-line),
implemented via `_PromptSymbolMixin` and `InputMode.MULTILINE`. The widget uses
@@ -107,6 +118,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- Fixed stale `AUTO-BUG-POOL` tracking prefix references in automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).
- **`agents session list` now displays full 26-character session ULIDs** (#10970): The Rich table
and Summary panel ("Most Recent" / "Oldest") previously showed only the first 8 characters of
each session ULID. This made the output unusable for copy-paste into `session tell`,
@@ -131,6 +144,11 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Security
- **PyYAML upgraded to >=6.0.3 to address known security vulnerability** (#9055):
Added an explicit `pyyaml>=6.0.3` dependency constraint to `pyproject.toml` to
prevent installation of vulnerable older versions with known YAML parsing security
issues.
- **aiohttp upgraded to >=3.13.4 to remediate CVE-2026-34513 and CVE-2026-34515** (#1549, #1544):
Added an explicit `aiohttp>=3.13.4` dependency constraint to `pyproject.toml` to remediate
two high-severity open redirect vulnerabilities. Both CVEs affect the CleverAgents platform's
@@ -151,6 +169,14 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
and milestone assignment. This eliminates systemic PR merge blockers caused by workers
omitting required items.
- **Implementation Pool Supervisor PR Compliance Checklist** (#9824): Added a mandatory
8-item PR Compliance Checklist to the new `implementation-pool-supervisor.md` agent definition.
Supervisors must enforce that workers complete all 8 checklist items (CHANGELOG.md update,
CONTRIBUTORS.md update, commit footer, CI verification, BDD tests, Epic reference, label
application, and milestone assignment) before creating any PR. Includes concrete markdown
examples for each subsection and compliance verification pseudocode to ensure reproducible
adherence.
- **ACMS context path matching now handles absolute fragment paths** (#10972): Fixed
`_path_matches()` in `execute_phase_context_assembler.py` and `_matches_pattern()` in
`context_phase_analysis.py` to correctly match absolute paths (e.g. `/app/.opencode/skills/SKILL.md`)
@@ -223,6 +249,25 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
failure paths. Comprehensive BDD test coverage validates the fix under concurrent
execution and confirms proper cleanup behavior.
- **Database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy** (#8608):
Implemented comprehensive database resource support enabling users to interact with
PostgreSQL and SQLite backends through a unified resource interface. Introduces
`DatabaseResourceHandler` providing full CRUD operations (`read`, `write`, `delete`,
`list_children`), connection validation with automatic credential masking via
:mod:`cleveragents.shared.redaction`, and transaction-based sandbox strategy using
BEGIN/COMMIT/ROLLBACK wrappers for safe, isolated database operations. SQLite-specific
checkpoint and rollback support with SAVEPOINT semantics. Support for multiple backends (PostgreSQL, SQLite, MySQL, DuckDB) via unified "DatabaseResourceHandler" and type-specific routing. BDD test
coverage in ``features/database_resources.feature`` (connection validation, CRUD workflows,
transaction/rollback behavior, error handling, credential masking verification) and
Robot Framework integration tests in ``robot/database_resources.robot``.
- **TransactionSandbox infrastructure for database resource isolation** (#8608):
Implemented ``TransactionSandbox`` class with BEGIN/COMMIT/ROLLBACK lifecycle
management for transaction-based sandbox strategy. Wired into ``SandboxFactory``
as the strategy resolver for database resource types. Added ``database`` resource type
registration in bootstrap builtin types and updated ``_resource_registry_data.py``
to recognize database resource categories.
### Fixed
- **fix(repositories): derive PlanResult.success from result_success column instead of error_message** (#7501):
@@ -423,6 +468,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
MCP logger thread-safety in `session.py` using a threading lock. Created
migration guide for users transitioning from legacy to V3 workflow.
- **Plan Tree JSON/YAML Command Envelope** (#9163): `agents plan tree --format json/yaml`
now wraps output in the spec-required command envelope with `command`, `status`,
`exit_code`, `data`, `timing`, and `messages` fields. The `data` field contains
`plan_id`, `tree`, `summary` (nodes, depth, child_plans, invariants, superseded),
`child_plans` list, and `decision_ids` mapping. Timing now reflects actual elapsed
milliseconds from command start to envelope construction.
- **Automation Profile Silent Fallback** (#8232): `_resolve_profile_for_plan` in
`PlanLifecycleService` now raises a clear `ValidationError` when a plan's
automation profile name is not a known built-in profile, instead of silently
@@ -656,6 +708,13 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
delays from 15s to 2s, and reduced idle sleep from 60s to 10s for dramatically
faster throughput.
- **Specification — Validation Gate Empty-Run Guard** (#8146): Updated `docs/specification.md`
to document the security invariant introduced in PR #7786 (fixing issue #7508). The spec now
explicitly states that `ApplyValidationSummary.all_required_passed` returns `False` when no
validations have been run (empty summary), blocking apply. Added a prominent danger admonition
block, updated the validation process results section, the `final_validation_results` data
model description, and two milestone acceptance criteria to reflect the corrected blocking
behavior for empty validation summaries and no-attachment runs.
### Fixed
+8 -5
View File
@@ -17,11 +17,12 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
* HAL 9000 has contributed concurrency safety improvements, including thread-safe context tier management (issue #7547) for parallel plan execution.
* HAL 9000 has contributed the plan concurrency race-condition fix (#7989): wired `LockService` into the plan lifecycle, guarding `execute_plan()` and `apply_plan()` with plan-level advisory locks and unique per-invocation owner identities to prevent silent concurrent state corruption.
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix: updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
<<<<<<< HEAD
* HAL 9000 has contributed the bug-hunt-pool-supervisor non-blocking tracking fix (#7875 / PR #7957): updated step 5 to be best-effort and added rule 9 to prevent the automation-tracking-manager call from blocking the main supervisor loop.
* Jeffrey Phillips Freeman has contributed the complete AUTO-BUG-POOL to AUTO-BUG-SUP tracking prefix fix across agent-system-specification.md, automation-tracking.md documentation and agent-system-specification.md spec document, replaced with correct `AUTO-BUG-SUP` prefix used by the bug-hunt-pool-supervisor agent (#7875).
* HAL 9000 has contributed the plugin entry point security hardening fix (#7476): enforced entry point allowlist validation before importing plugin modules to prevent malicious plugin loading.
* HAL 9000 has contributed the benchmark workflow separation (#9040): moved the benchmark-regression job out of the default PR workflow into a dedicated scheduled workflow, reducing median PR CI turnaround time from 99-132 minutes to under 30 minutes.
* HAL 9000 has contributed the agent-evolution-pool-supervisor PR metadata assignment (#7888): the supervisor now automatically looks up the Type/Automation label and earliest open milestone before dispatching improvement PR creation workers, ensuring all generated improvement PRs have correct Type labels and milestone assignments.
* HAL 9000 has contributed the decision recording hook for the Strategize phase (issue #8522): captures every decision point with question, chosen option, alternatives, confidence, rationale, and full context snapshot for replay and correction.
* HAL 9000 has contributed the plan tree JSON/YAML command envelope fix (#9163): wrapped `agents plan tree --format json/yaml` output in the spec-required command envelope structure, added summary statistics, decision_ids mapping, child_plans list, and accurate timing measurement.
* This project was made possible thanks to considerable donation of time, money, and resources by CleverThis, Inc.
* HAL 9000 has contributed automated bug fixes, CLI output formatting improvements, and ongoing maintenance as part of the CleverAgents automation system.
* HAL 9000 has contributed the file edit encoding parameter fix (PR #8258 / issue #7559).
@@ -30,11 +31,13 @@ Below are some of the specific details of various contributions.
* HAL 9000 has contributed the git_tools TOCTOU race condition fix (PR #8255 / issue #7619): eliminated the Time-Of-Check-To-Time-Of-Use race in `_get_base_env()` by adding double-checked locking with a module-level `threading.Lock`, preventing concurrent threads from writing conflicting environment snapshots.
* HAL 9000 has contributed the mandatory PR compliance checklist to `implementation-supervisor.md` (#9824): added an 8-item checklist to the worker prompt body with concrete items covering CHANGELOG.md, CONTRIBUTORS.md, commit footer, CI verification, BDD tests, Epic reference, labels, and milestone assignment to eliminate systemic PR merge blockers.
* HAL 9000 has contributed the PlanResult.success derivation fix (PR #8214 / issue #7501): replaced the incorrect `error_message is None` heuristic with a dedicated `result_success` column in the plans table, ensuring plans with historical build errors are not incorrectly marked as failed after a successful apply.
* HAL 9000 has contributed the mandatory PR compliance checklist to `implementation-pool-supervisor.md` (#9824): created a new agent definition with an embedded 8-item checklist ensuring workers always update CHANGELOG.md, CONTRIBUTORS.md, include commit footers (`ISSUES CLOSED: #N`), verify CI passes, add BDD tests, reference the parent Epic, apply labels via forgejo-label-manager, and assign milestones before creating PRs. Includes concrete examples for each subsection and compliance verification pseudocode.
* HAL 9000 has contributed comprehensive milestone documentation for v3.6.0 (Advanced Concepts & Deferred Features) and v3.7.0 (TUI Implementation) (PR #9903): split into sub-documents covering context strategies, LLM backends, resource types, A2A rename, container tool execution, scope chain resolution, cost/safety budgets, E2E workflow tests, code review examples, plugin architecture, TUI layout, persona system, reference/command input, session management, configuration, and TuiMaterializer integration.
* HAL 9000 has contributed the LLMTraceRepository data-integrity fix (PR #8185 / issue #7505): replaced the unconditional `session.commit()` in `LLMTraceRepository.save()` with a dual-path implementation that respects the UnitOfWork pattern — flushing only when an external session is provided, and flushing + committing + closing when operating standalone. This eliminates premature transaction commits, loss of rollback capability, and a docstring/implementation mismatch.
* HAL 9000 has contributed the ACMS Index Data Model and File Traversal Engine (PR #9664 / issue #9579): foundational data structures for indexed context entries with hot/warm/cold/archive storage tier classification, tag system, and a timeout-safe chunked file traversal engine for large projects with 10,000+ files.
* HAL 9000 has contributed the error-suppression removal fix (PR #9247 / issue #9060): removed both `try...except Exception:` blocks in `register_registry_agents()` that silently suppressed errors from `actor_registry.list_actors()` and the route bridge refresh, enabling exceptions to propagate per CONTRIBUTING.md fail-fast policy. Added three Behave scenarios verifying RuntimeError, AttributeError, and TypeError propagation.
* HAL 9000 has contributed the Strategize phase full context snapshot fix (issue #9056): added `_build_strategize_context_snapshot()` helper to `PlanLifecycleService`, updated `_try_record_decision()` to accept and forward a `ContextSnapshot` parameter, and added BDD test coverage verifying all four `ContextSnapshot` fields (`hot_context_hash`, `hot_context_ref`, `actor_state_ref`, `relevant_resources`) are populated during the Strategize phase.
* HAL 9000 has contributed the ACMS context path matching fix (PR #10975 / issue #10972): corrects `_path_matches()` and `_matches_pattern()` to properly match absolute fragment paths against relative glob patterns by auto-prefixing with `**/` before calling `PurePath.full_match()`, preventing silent inefficacy of include/exclude filters for absolute paths in fragment metadata.
* HAL 9000 has contributed the real LLM actor invocation for session tell (PR #10979 / issue #5784): replaced the echo-stub in `agents session tell` with full orchestrator-based LLM invocation via `SessionWorkflow`, wiring `LangChainSessionCaller``ToolCallingRuntime.run_tool_loop()`. Added token usage tracking, cost estimation, streaming support, and A2A protocol compliance through the local facade. BDD test coverage includes six Be happy scenarios covering real response persistence, streaming, error handling, actor override, JSON output, and rich usage panel rendering.
* HAL 9000 has contributed database resource types (PostgreSQL, SQLite) with transaction-based sandbox strategy: implemented ``DatabaseResourceHandler`` providing full CRUD operations (`read`, `write`, `delete`, `list_children`) and connection validation with automatic credential masking for PostgreSQL and SQLite backends. Includes ``TransactionSandbox`` infrastructure wired into ``SandboxFactory``, BDD test coverage in ``features/database_resources.feature``, and Robot Framework integration tests in ``robot/database_resources.robot`` (PR #10591 / issue #8608, Epic #8568).
* HAL 9000 has contributed the agents plan rollback command (PR #8674 / issue #8557): implemented checkpoint-based plan state restoration with the `agents plan rollback <plan-id> [<checkpoint-id>]` CLI command as part of Epic #8493, enabling plans to be restored to previous checkpoints, discarding post-checkpoint decisions, and resuming execution from the rolled-back state. Supported by `--yes/-y`, `--to-checkpoint`, and `--format/-f` flags. Includes comprehensive BDD test coverage (>= 97%) for rollback, decision discarding, and plan resume functionality.
* HAL 9000 has contributed the PyYAML security upgrade (PR #11012 / issue #9055): added `pyyaml>=6.0.3` dependency constraint to address known YAML parsing vulnerabilities.
+264
View File
@@ -0,0 +1,264 @@
"""ASV benchmarks for circuit breaker pattern.
Measures the overhead of circuit breaker state management,
state transitions, and fast-fail latency in open state.
"""
from __future__ import annotations
import asyncio
import time
from cleveragents.core.circuit_breaker import (
CircuitBreaker,
CircuitBreakerOpen,
CircuitBreakerState,
)
class CircuitBreakerClosedStateBench:
"""Benchmark circuit breaker overhead in closed state."""
timeout = 60
def setup(self) -> None:
"""Set up circuit breaker in closed state."""
self.breaker = CircuitBreaker(
failure_threshold=5,
recovery_timeout=60.0,
expected_exception=Exception,
half_open_max_successes=2,
cooldown_seconds=30.0,
name="test_service",
)
def time_call_success(self) -> None:
"""Benchmark successful call overhead in closed state."""
def dummy_func() -> str:
return "success"
self.breaker.call(dummy_func)
def time_call_with_args(self) -> None:
"""Benchmark call with arguments in closed state."""
def dummy_func(x: int, y: str) -> str:
return f"{x}:{y}"
self.breaker.call(dummy_func, 42, "test")
def time_state_check(self) -> None:
"""Benchmark state property access."""
_ = self.breaker.state
def time_failure_count_check(self) -> None:
"""Benchmark failure count property access."""
_ = self.breaker.failure_count
class CircuitBreakerOpenStateBench:
"""Benchmark circuit breaker fast-fail latency in open state."""
timeout = 60
def setup(self) -> None:
"""Set up circuit breaker in open state."""
self.breaker = CircuitBreaker(
failure_threshold=2,
recovery_timeout=60.0,
expected_exception=Exception,
half_open_max_successes=2,
cooldown_seconds=30.0,
name="test_service",
)
# Force the breaker into open state
for _ in range(3):
try:
self.breaker.call(lambda: 1 / 0) # Raise ZeroDivisionError
except ZeroDivisionError:
pass
def time_fast_fail(self) -> None:
"""Benchmark fast-fail latency when circuit is open."""
try:
self.breaker.call(lambda: "should not execute")
except CircuitBreakerOpen:
pass
def time_open_state_check(self) -> None:
"""Benchmark state check when open."""
assert self.breaker.state == CircuitBreakerState.OPEN
class CircuitBreakerClosedToOpenTransitionBench:
"""Benchmark closed-to-open state transition overhead."""
timeout = 60
def setup(self) -> None:
"""Set up a fresh circuit breaker in closed state."""
self.breaker = CircuitBreaker(
failure_threshold=2,
recovery_timeout=60.0,
expected_exception=Exception,
half_open_max_successes=1,
cooldown_seconds=30.0,
name="test_service",
)
def time_closed_to_open_transition(self) -> None:
"""Benchmark transition from closed to open state."""
# Trigger failures to transition to open
for _ in range(3):
try:
self.breaker.call(lambda: 1 / 0)
except ZeroDivisionError:
pass
class CircuitBreakerOpenToHalfOpenTransitionBench:
"""Benchmark open-to-half-open state transition overhead."""
timeout = 60
def setup(self) -> None:
"""Set up circuit breaker already in open state."""
self.breaker = CircuitBreaker(
failure_threshold=2,
recovery_timeout=0.05, # Very short timeout
expected_exception=Exception,
half_open_max_successes=1,
cooldown_seconds=0.01,
name="test_service",
)
# Force to open state
for _ in range(3):
try:
self.breaker.call(lambda: 1 / 0)
except ZeroDivisionError:
pass
# Wait for recovery timeout so transition to half-open is possible
time.sleep(0.1)
def time_open_to_half_open_transition(self) -> None:
"""Benchmark transition from open to half-open state."""
# Attempt call to trigger half-open transition
try:
self.breaker.call(lambda: "success")
except CircuitBreakerOpen:
pass
class CircuitBreakerHalfOpenToClosedTransitionBench:
"""Benchmark half-open-to-closed state transition overhead."""
timeout = 60
def setup(self) -> None:
"""Set up circuit breaker already in half-open state."""
self.breaker = CircuitBreaker(
failure_threshold=2,
recovery_timeout=0.05,
expected_exception=Exception,
half_open_max_successes=1,
cooldown_seconds=0.01,
name="test_service",
)
# Force to open state
for _ in range(3):
try:
self.breaker.call(lambda: 1 / 0)
except ZeroDivisionError:
pass
# Wait for recovery timeout so breaker enters half-open on next call
time.sleep(0.1)
def time_half_open_to_closed_transition(self) -> None:
"""Benchmark transition from half-open to closed state."""
# Successful call in half-open should close the breaker
self.breaker.call(lambda: "success")
class CircuitBreakerAsyncBench:
"""Benchmark async circuit breaker operations."""
timeout = 60
def setup(self) -> None:
"""Set up async circuit breakers."""
self.breaker = CircuitBreaker(
failure_threshold=5,
recovery_timeout=60.0,
expected_exception=Exception,
half_open_max_successes=2,
cooldown_seconds=30.0,
name="test_service",
)
# Set up a separate breaker already in open state for fast-fail benchmark
self.open_breaker = CircuitBreaker(
failure_threshold=2,
recovery_timeout=60.0,
expected_exception=Exception,
half_open_max_successes=2,
cooldown_seconds=30.0,
name="test_service_open",
)
for _ in range(3):
try:
self.open_breaker.call(lambda: 1 / 0)
except ZeroDivisionError:
pass
def time_async_call_success(self) -> None:
"""Benchmark successful async call overhead."""
async def dummy_async() -> str:
return "success"
asyncio.run(self.breaker.async_call(dummy_async))
def time_async_call_with_args(self) -> None:
"""Benchmark async call with arguments."""
async def dummy_async(x: int, y: str) -> str:
return f"{x}:{y}"
asyncio.run(self.breaker.async_call(dummy_async, 42, "test"))
def time_async_fast_fail(self) -> None:
"""Benchmark async fast-fail when open."""
async def dummy_async() -> str:
return "should not execute"
try:
asyncio.run(self.open_breaker.async_call(dummy_async))
except CircuitBreakerOpen:
pass
class CircuitBreakerInitializationBench:
"""Benchmark circuit breaker initialization overhead."""
timeout = 60
def time_default_initialization(self) -> None:
"""Benchmark default initialization."""
CircuitBreaker()
def time_custom_initialization(self) -> None:
"""Benchmark initialization with custom parameters."""
CircuitBreaker(
failure_threshold=10,
recovery_timeout=120.0,
expected_exception=ValueError,
half_open_max_successes=3,
cooldown_seconds=45.0,
name="custom_service",
)
def time_multiple_breakers(self) -> None:
"""Benchmark creating multiple breakers."""
for i in range(10):
CircuitBreaker(name=f"service_{i}")
+280
View File
@@ -0,0 +1,280 @@
"""ASV benchmarks for retry patterns.
Measures the overhead of retry decorator construction and
happy-path invocation for the public retry factory functions.
"""
from __future__ import annotations
import asyncio
from cleveragents.core.retry_patterns import (
get_retry_decorator,
retry_on_result,
retry_with_exponential_backoff,
retry_with_jitter,
retry_with_timeout,
should_retry_result,
)
class RetryDecoratorConstructionBench:
"""Benchmark retry decorator construction overhead."""
timeout = 60
def time_retry_with_exponential_backoff_construction(self) -> None:
"""Benchmark constructing exponential backoff decorator."""
@retry_with_exponential_backoff(max_attempts=3)
def dummy_func() -> str:
return "success"
def time_retry_with_jitter_construction(self) -> None:
"""Benchmark constructing jitter decorator."""
@retry_with_jitter(max_attempts=3)
def dummy_func() -> str:
return "success"
def time_retry_with_timeout_construction(self) -> None:
"""Benchmark constructing timeout decorator."""
@retry_with_timeout(timeout_seconds=5.0)
def dummy_func() -> str:
return "success"
def time_retry_on_result_construction(self) -> None:
"""Benchmark constructing result-based retry decorator."""
@retry_on_result(should_retry_result, max_attempts=3)
def dummy_func() -> str:
return "success"
def time_get_retry_decorator_network(self) -> None:
"""Benchmark constructing decorator via factory for network category."""
get_retry_decorator("network")
def time_get_retry_decorator_provider(self) -> None:
"""Benchmark constructing decorator via factory for provider category."""
get_retry_decorator("provider")
def time_get_retry_decorator_database(self) -> None:
"""Benchmark constructing decorator via factory for database category."""
get_retry_decorator("database")
def time_get_retry_decorator_unknown(self) -> None:
"""Benchmark constructing decorator via factory for unknown category."""
get_retry_decorator("unknown_category")
class RetryDecoratorInvocationBench:
"""Benchmark happy-path retry decorator invocation."""
timeout = 60
def setup(self) -> None:
"""Set up decorated functions."""
@retry_with_exponential_backoff(max_attempts=3)
def exponential_func() -> str:
return "success"
@retry_with_jitter(max_attempts=3)
def jitter_func() -> str:
return "success"
@retry_with_timeout(timeout_seconds=5.0)
def timeout_func() -> str:
return "success"
@retry_on_result(should_retry_result, max_attempts=3)
def result_func() -> str:
return "success"
self.exponential_func = exponential_func
self.jitter_func = jitter_func
self.timeout_func = timeout_func
self.result_func = result_func
def time_exponential_backoff_invocation(self) -> None:
"""Benchmark exponential backoff invocation (happy path)."""
self.exponential_func()
def time_jitter_invocation(self) -> None:
"""Benchmark jitter invocation (happy path)."""
self.jitter_func()
def time_timeout_invocation(self) -> None:
"""Benchmark timeout invocation (happy path)."""
self.timeout_func()
def time_result_based_invocation(self) -> None:
"""Benchmark result-based retry invocation (happy path)."""
self.result_func()
class RetryDecoratorAsyncBench:
"""Benchmark async retry decorator operations."""
timeout = 60
def setup(self) -> None:
"""Set up async decorated functions."""
@retry_with_exponential_backoff(max_attempts=3)
async def async_exponential() -> str:
return "success"
@retry_with_jitter(max_attempts=3)
async def async_jitter() -> str:
return "success"
@retry_with_timeout(timeout_seconds=5.0)
async def async_timeout() -> str:
return "success"
self.async_exponential = async_exponential
self.async_jitter = async_jitter
self.async_timeout = async_timeout
def time_async_exponential_invocation(self) -> None:
"""Benchmark async exponential backoff invocation."""
asyncio.run(self.async_exponential())
def time_async_jitter_invocation(self) -> None:
"""Benchmark async jitter invocation."""
asyncio.run(self.async_jitter())
def time_async_timeout_invocation(self) -> None:
"""Benchmark async timeout invocation."""
asyncio.run(self.async_timeout())
class RetryDecoratorWithArgumentsBench:
"""Benchmark retry decorators with function arguments."""
timeout = 60
def setup(self) -> None:
"""Set up decorated functions with arguments."""
@retry_with_exponential_backoff(max_attempts=3)
def func_with_args(x: int, y: str, z: float = 1.0) -> str:
return f"{x}:{y}:{z}"
@retry_with_jitter(max_attempts=3)
def func_with_kwargs(a: int, b: str = "default") -> str:
return f"{a}:{b}"
self.func_with_args = func_with_args
self.func_with_kwargs = func_with_kwargs
def time_exponential_with_positional_args(self) -> None:
"""Benchmark exponential backoff with positional arguments."""
self.func_with_args(42, "test", 2.5)
def time_exponential_with_keyword_args(self) -> None:
"""Benchmark exponential backoff with keyword arguments."""
self.func_with_args(x=42, y="test", z=2.5)
def time_jitter_with_mixed_args(self) -> None:
"""Benchmark jitter with mixed positional and keyword arguments."""
self.func_with_kwargs(100, b="custom")
class RetryDecoratorFactoryBench:
"""Benchmark retry decorator factory function."""
timeout = 60
def time_factory_network_category(self) -> None:
"""Benchmark factory with network category."""
decorator = get_retry_decorator("network")
@decorator
def dummy() -> str:
return "success"
dummy()
def time_factory_provider_category(self) -> None:
"""Benchmark factory with provider category."""
decorator = get_retry_decorator("provider")
@decorator
def dummy() -> str:
return "success"
dummy()
def time_factory_database_category(self) -> None:
"""Benchmark factory with database category."""
decorator = get_retry_decorator("database")
@decorator
def dummy() -> str:
return "success"
dummy()
def time_factory_file_operation_category(self) -> None:
"""Benchmark factory with file_operation category."""
decorator = get_retry_decorator("file_operation")
@decorator
def dummy() -> str:
return "success"
dummy()
class RetryDecoratorConfigurationBench:
"""Benchmark retry decorator with various configurations."""
timeout = 60
def time_minimal_config(self) -> None:
"""Benchmark decorator with minimal configuration."""
@retry_with_exponential_backoff()
def dummy() -> str:
return "success"
dummy()
def time_aggressive_config(self) -> None:
"""Benchmark decorator with aggressive retry settings."""
@retry_with_exponential_backoff(max_attempts=10)
def dummy() -> str:
return "success"
dummy()
def time_conservative_config(self) -> None:
"""Benchmark decorator with conservative retry settings."""
@retry_with_exponential_backoff(max_attempts=1)
def dummy() -> str:
return "success"
dummy()
def time_long_timeout_config(self) -> None:
"""Benchmark decorator with long timeout."""
@retry_with_timeout(timeout_seconds=300.0)
def dummy() -> str:
return "success"
dummy()
def time_short_timeout_config(self) -> None:
"""Benchmark decorator with short timeout."""
@retry_with_timeout(timeout_seconds=0.1)
def dummy() -> str:
return "success"
dummy()
@@ -0,0 +1,367 @@
"""ASV benchmarks for service-level retry patterns.
Measures the overhead of retry_service_operation decorator
and retry_auto_debug happy-path latency.
"""
from __future__ import annotations
import asyncio
from typing import Any
from cleveragents.core.circuit_breaker import CircuitBreaker
from cleveragents.core.retry_service_patterns import (
RetryContext,
is_read_only_plan_operation,
retry_auto_debug,
retry_service_operation,
)
class RetryServiceOperationDecoratorBench:
"""Benchmark retry_service_operation decorator overhead."""
timeout = 60
def time_decorator_construction_sync(self) -> None:
"""Benchmark constructing sync service retry decorator."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
)
def dummy_func() -> str:
return "success"
def time_decorator_construction_async(self) -> None:
"""Benchmark constructing async service retry decorator."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
)
async def dummy_func() -> str:
return "success"
def time_decorator_with_circuit_breaker(self) -> None:
"""Benchmark decorator with circuit breaker instance."""
cb = CircuitBreaker(
failure_threshold=5,
recovery_timeout=60.0,
name="test_service",
)
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
circuit_breaker=cb,
)
def dummy_func() -> str:
return "success"
def time_decorator_with_exponential_strategy(self) -> None:
"""Benchmark decorator with exponential backoff strategy."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
backoff_strategy="exponential",
base_delay=1.0,
max_delay=60.0,
)
def dummy_func() -> str:
return "success"
class RetryServiceOperationInvocationBench:
"""Benchmark happy-path service retry invocation."""
timeout = 60
def setup(self) -> None:
"""Set up decorated service operations."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
)
def sync_op() -> str:
return "success"
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
)
async def async_op() -> str:
return "success"
cb = CircuitBreaker(
failure_threshold=5,
recovery_timeout=60.0,
name="test_service",
)
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
circuit_breaker=cb,
)
def breaker_op() -> str:
return "success"
self.sync_op = sync_op
self.async_op = async_op
self.breaker_op = breaker_op
def time_sync_operation_invocation(self) -> None:
"""Benchmark sync service operation invocation (happy path)."""
self.sync_op()
def time_async_operation_invocation(self) -> None:
"""Benchmark async service operation invocation (happy path)."""
asyncio.run(self.async_op())
def time_operation_with_circuit_breaker(self) -> None:
"""Benchmark service operation with circuit breaker (happy path)."""
self.breaker_op()
class RetryServiceOperationWithArgumentsBench:
"""Benchmark service retry with function arguments."""
timeout = 60
def setup(self) -> None:
"""Set up decorated operations with arguments."""
@retry_service_operation(
service_name="test_service",
operation_name="fetch_data",
max_attempts=3,
)
def fetch_data(
resource_id: int, include_metadata: bool = False
) -> dict[str, Any]:
return {"id": resource_id, "data": "test"}
@retry_service_operation(
service_name="test_service",
operation_name="update_data",
max_attempts=3,
)
async def update_data(
resource_id: int, payload: dict[str, Any]
) -> dict[str, Any]:
return {"id": resource_id, "updated": True}
self.fetch_data = fetch_data
self.update_data = update_data
def time_sync_with_positional_args(self) -> None:
"""Benchmark sync operation with positional arguments."""
self.fetch_data(42, True)
def time_sync_with_keyword_args(self) -> None:
"""Benchmark sync operation with keyword arguments."""
self.fetch_data(resource_id=42, include_metadata=True)
def time_async_with_complex_payload(self) -> None:
"""Benchmark async operation with complex payload."""
payload = {"key": "value", "nested": {"data": [1, 2, 3]}}
asyncio.run(self.update_data(42, payload))
class RetryAutoDebugBench:
"""Benchmark retry_auto_debug decorator."""
timeout = 60
def time_auto_debug_construction(self) -> None:
"""Benchmark constructing auto-debug decorator."""
@retry_auto_debug(max_debug_attempts=3)
async def dummy_func() -> str:
return "success"
def time_auto_debug_invocation(self) -> None:
"""Benchmark auto-debug invocation (happy path)."""
@retry_auto_debug(max_debug_attempts=3)
async def dummy_func() -> str:
return "success"
asyncio.run(dummy_func())
def time_auto_debug_single_attempt(self) -> None:
"""Benchmark auto-debug with single attempt."""
@retry_auto_debug(max_debug_attempts=1)
async def dummy_func() -> str:
return "success"
asyncio.run(dummy_func())
def time_auto_debug_multiple_attempts(self) -> None:
"""Benchmark auto-debug with multiple attempts configured."""
@retry_auto_debug(max_debug_attempts=5)
async def dummy_func() -> str:
return "success"
asyncio.run(dummy_func())
class RetryContextBench:
"""Benchmark RetryContext operations."""
timeout = 60
def time_context_creation(self) -> None:
"""Benchmark creating a retry context."""
RetryContext(
operation_name="test_op",
max_attempts=3,
)
def time_context_with_custom_wait(self) -> None:
"""Benchmark context with custom wait strategy."""
from tenacity import wait_exponential
RetryContext(
operation_name="test_op",
max_attempts=3,
wait_strategy=wait_exponential(multiplier=1, min=1, max=30),
)
def time_context_property_access(self) -> None:
"""Benchmark accessing context properties."""
ctx = RetryContext(
operation_name="test_op",
max_attempts=3,
)
_ = ctx.operation_name
_ = ctx.max_attempts
_ = ctx.attempt_count
def time_context_execute(self) -> None:
"""Benchmark executing a function via RetryContext."""
ctx = RetryContext(
operation_name="test_op",
max_attempts=3,
)
ctx.execute(lambda: "success")
class IsReadOnlyPlanOperationBench:
"""Benchmark is_read_only_plan_operation utility."""
timeout = 60
def time_check_strategize(self) -> None:
"""Benchmark checking strategize operation."""
is_read_only_plan_operation({"plan_phase": "strategize"})
def time_check_plan(self) -> None:
"""Benchmark checking plan operation."""
is_read_only_plan_operation({"plan_phase": "plan"})
def time_check_validate(self) -> None:
"""Benchmark checking validate operation."""
is_read_only_plan_operation({"plan_phase": "validate"})
def time_check_review(self) -> None:
"""Benchmark checking review operation."""
is_read_only_plan_operation({"plan_phase": "review"})
def time_check_preview(self) -> None:
"""Benchmark checking preview operation."""
is_read_only_plan_operation({"plan_phase": "preview"})
def time_check_check(self) -> None:
"""Benchmark checking check operation."""
is_read_only_plan_operation({"plan_phase": "check"})
def time_check_execute(self) -> None:
"""Benchmark checking execute operation (not read-only)."""
is_read_only_plan_operation({"plan_phase": "execute"})
def time_check_read_only_flag(self) -> None:
"""Benchmark checking read_only flag."""
is_read_only_plan_operation({"read_only": True})
def time_check_unknown_operation(self) -> None:
"""Benchmark checking unknown operation."""
is_read_only_plan_operation({"plan_phase": "unknown_op"})
def time_check_empty_kwargs(self) -> None:
"""Benchmark checking empty kwargs."""
is_read_only_plan_operation({})
class RetryServiceOperationStrategyBench:
"""Benchmark different retry strategies in service operations."""
timeout = 60
def time_exponential_strategy(self) -> None:
"""Benchmark exponential strategy."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
backoff_strategy="exponential",
)
def dummy() -> str:
return "success"
dummy()
def time_linear_strategy(self) -> None:
"""Benchmark linear strategy."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
backoff_strategy="linear",
)
def dummy() -> str:
return "success"
dummy()
def time_fixed_strategy(self) -> None:
"""Benchmark fixed strategy."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
backoff_strategy="fixed",
)
def dummy() -> str:
return "success"
dummy()
def time_jitter_strategy(self) -> None:
"""Benchmark jitter strategy."""
@retry_service_operation(
service_name="test_service",
operation_name="test_op",
max_attempts=3,
backoff_strategy="jitter",
)
def dummy() -> str:
return "success"
dummy()
@@ -665,7 +665,7 @@ The Automation Tracking Manager provides eleven operations:
| `AUTO-IMP-POOL` | Implementation Pool Supervisor |
| `AUTO-REV-POOL` | PR Review Pool Supervisor |
| `AUTO-UAT-POOL` | UAT Test Pool Supervisor |
| `AUTO-BUG-POOL` | Bug Hunt Pool Supervisor |
| `AUTO-BUG-SUP` | Bug Hunt Pool Supervisor |
| `AUTO-INF-POOL` | Test Infrastructure Pool Supervisor |
| `AUTO-ARCH` | Architecture Supervisor |
| `AUTO-EPIC` | Epic Planning Supervisor |
@@ -693,7 +693,7 @@ The following table maps between the two schemes for agents where they differ:
| Implementation Pool | `AUTO-IMP-SUP` | `AUTO-IMP-POOL` |
| PR Review Pool | `AUTO-REV-SUP` | `AUTO-REV-POOL` |
| UAT Test Pool | `AUTO-UAT-SUP` | `AUTO-UAT-POOL` |
| Bug Hunt Pool | `AUTO-BUG-SUP` | `AUTO-BUG-POOL` |
| Bug Hunt Pool | `AUTO-BUG-SUP` | `AUTO-BUG-SUP` |
| Test Infrastructure Pool | `AUTO-INF-SUP` | `AUTO-INF-POOL` |
| Human Liaison | `AUTO-HUMAN` | `AUTO-LIAISON` |
| Grooming | `AUTO-GROOM` | `AUTO-GROOMER` |
@@ -1869,7 +1869,7 @@ Only critical bugs get assigned to the active milestone. Non-critical issues go
| **Mode** | `subagent` |
| **Model** | `google/gemini-2.5-pro` |
| **Temperature** | 0.1 |
| **Tracking Prefix** | `AUTO-BUG-POOL` |
| **Tracking Prefix** | `AUTO-BUG-SUP` |
| **Worker Count** | N/4 (quarter allocation) |
#### 8.6.1 Purpose
@@ -3164,7 +3164,7 @@ Defines how ALL agents discover, interact with, and coordinate through the autom
### 13.9 Bug Hunter Tracking Update (`shared/bug_hunter_tracking_update.md`)
Defines the migration guide for converting the Bug Hunt supervisor from the old session state system to the new individual tracking issue system. Documents the tracking issue format `[AUTO-BUG-POOL] Bug Detection Pool Status (Cycle N)`, the cleanup protocol (one issue per cycle, preserve announcements), and worker coordination via announcement issues.
Defines the migration guide for converting the Bug Hunt supervisor from the old session state system to the new individual tracking issue system. Documents the tracking issue format `[AUTO-BUG-SUP] Bug Hunt Status (Cycle 25)`, the cleanup protocol (one issue per cycle, preserve announcements), and worker coordination via announcement issues.
---
@@ -6767,7 +6767,7 @@ The system uses five distinct redundancy patterns:
| 4 | pr-merge-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-MERGE | Singleton Supervisor |
| 5 | pr-fix-pool-supervisor | *(removed — absorbed into implementation-pool-supervisor)* | -- | -- | -- |
| 6 | uat-test-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-UAT-POOL | Pool Supervisor |
| 7 | bug-hunt-pool-supervisor | subagent | gemini-2.5-pro | AUTO-BUG-POOL | Pool Supervisor |
| 7 | bug-hunt-pool-supervisor | subagent | gemini-2.5-pro | AUTO-BUG-SUP | Pool Supervisor |
| 8 | test-infra-pool-supervisor | subagent | gemini-2.5-pro | AUTO-INF-POOL | Pool Supervisor |
| 9 | architecture-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-ARCH | Singleton Supervisor |
| 10 | epic-planning-pool-supervisor | subagent | claude-sonnet-4-6 | AUTO-EPIC | Singleton Supervisor |
+2 -2
View File
@@ -61,7 +61,7 @@ All prefixes are registered in the `automation-tracking-manager` subagent, which
| uat-test-pool-supervisor | `AUTO-UAT-POOL` | `[AUTO-UAT-POOL] UAT Status (Cycle 6)` |
| project-owner-pool-supervisor | `AUTO-PROJ-OWN` | `[AUTO-PROJ-OWN] Project Status (Cycle 11)` |
| agent-evolution-pool-supervisor | `AUTO-EVLV` | `[AUTO-EVLV] Agent Evolution Report (Cycle 10)` |
| bug-hunt-pool-supervisor | `AUTO-BUG-POOL` | `[AUTO-BUG-POOL] Bug Detection Pool Status (Cycle 60)` |
| bug-hunt-pool-supervisor | `AUTO-BUG-SUP` | `[AUTO-BUG-SUP] Bug Hunt Status (Cycle 25)` |
| spec-update-pool-supervisor | `AUTO-SPEC` | `[AUTO-SPEC] Specification Update Report (Cycle 1)` |
| test-infra-pool-supervisor | `AUTO-INF-POOL` | `[AUTO-INF-POOL] Infrastructure Analysis Report (Cycle 2)` |
| epic-planner | `AUTO-EPIC` | `[AUTO-EPIC] Epic Planning Update (Cycle 9)` |
@@ -394,7 +394,7 @@ label:"Automation Tracking" [AUTO-UAT-POOL] in:title
label:"Automation Tracking" [AUTO-PROJ-OWN] in:title
label:"Automation Tracking" [AUTO-EVLV] in:title
label:"Automation Tracking" [AUTO-EPIC] in:title
label:"Automation Tracking" [AUTO-BUG-POOL] in:title
label:"Automation Tracking" [AUTO-BUG-SUP] in:title
label:"Automation Tracking" [AUTO-SPEC] in:title
label:"Automation Tracking" [AUTO-INF-POOL] in:title
```
+17 -4
View File
@@ -20019,6 +20019,18 @@ Apply should perform (configurable) validations before committing:
Validation runs during **Execute**, not during Apply. By the time a plan reaches the Apply phase, all required validations have already passed. Apply commits the sandbox changes to the real resources and does not re-run validations.
!!! danger "Security Invariant — Empty Validation Summary Blocks Apply"
Apply **requires** that at least one required validation was executed during the Execute phase. If no validations were run — because no validations are attached to the plan's resources, or because the validation collection step produced an empty set — the apply gate **blocks** and the plan cannot proceed to Apply.
This is an intentional security invariant (introduced in PR #7786, fixing issue #7508): apply cannot proceed without at least one validation having run. A plan with no validation attachments is treated the same as a plan with failing validations — it cannot be applied.
**Blocking conditions for the apply gate**:
- Any required validation returned `passed: false` (validation failure)
- No required validations were executed at all (empty validation summary)
- A plan has no validation attachments (no validations configured)
The `ApplyValidationSummary.all_required_passed` property returns `True` **only when** at least one required validation was executed AND none of the required validations failed. An empty summary always yields `False`.
For full details on validation — including the Validation type system, modes, attachment scoping, failure handling, fix-then-revalidate loops, and data model — see the **Validation** section under Core Concepts.
#### Apply Data Model
@@ -22757,7 +22769,8 @@ Validation is the **final step** of the Execute phase. The execution actor's wor
2. **Collect applicable validations**: The system resolves all validations from the resource-direct, project, and plan scopes (as described in Attachment Resolution above).
3. **Execute validations**: Each applicable validation is invoked as a standard tool call. The validation reads the sandbox state and returns its structured result. Validations may be run in parallel since they are read-only and cannot interfere with each other.
4. **Process results**:
- **All required validations pass**: Execution is complete. The plan transitions to the review/Apply phase.
- **All required validations pass (and at least one ran)**: Execution is complete. The plan transitions to the review/Apply phase.
- **No required validations were executed (empty summary)**: The apply gate is blocked. This is treated equivalently to a required validation failure — the plan cannot proceed to Apply. This condition arises when no validations are attached to the plan's resources. See the security invariant note in [Validation in Apply](#validation-in-apply).
- **Any required validation fails**: The execution actor enters the fix-then-revalidate loop (see Validation Failure Handling below).
- **Informational validations fail**: Results are recorded in the plan's validation summary. No fix attempts are made.
@@ -23076,7 +23089,7 @@ The validation-related fields stored in the plan data model:
| Field | Location | Description |
|-------|----------|-------------|
| `validation_summary` | `plan.execution` | Array of validation results collected during Execute. Each entry includes the validation name, mode, `passed`, `message`, `data`, execution duration, and attempt number. |
| `final_validation_results` | `plan.apply` | Snapshot of the final validation state at the time execution completed (all required validations passing). Identical to the last state of `validation_summary` but stored separately for quick reference. |
| `final_validation_results` | `plan.apply` | Snapshot of the final validation state at the time execution completed (all required validations passing, and at least one required validation was executed). Identical to the last state of `validation_summary` but stored separately for quick reference. A plan can only reach the Apply phase if this snapshot is non-empty and all required entries have `passed: true`. |
| `validation_attempts` | `plan.execution` | Total number of validation invocations across all fix-then-revalidate loops. Useful for understanding how much effort was spent on validation remediation. |
| `validation_fix_history` | `plan.execution` | Per-validation log of fix attempts. Each entry records: the validation that failed, the fix the actor applied, and whether the subsequent re-validation passed. Enables auditing of the fix-then-revalidate process. |
@@ -47123,7 +47136,7 @@ This section defines the ordered milestone plan for CleverAgents v3.x, mapping a
| 6 | Hierarchical decomposition creates 4+ levels of subplans | §Subplan Architecture — Hierarchy | `plan tree` shows 4+ nesting levels for complex tasks |
| 7 | Parallel execution scales to 10+ concurrent subplans | §Subplan Execution — Parallel | 10 subplans execute concurrently without deadlock or resource starvation |
| 8 | Decision correction recomputes only affected subtree | §Correction Model — Selective Recomputation | Unaffected decisions preserved; only downstream decisions recomputed |
| 9 | Validation-gated apply: required validations must pass before apply | §Validation Abstraction — Apply Gating | Apply blocked when required validation fails; informational validation does not block |
| 9 | Validation-gated apply: required validations must pass before apply | §Validation Abstraction — Apply Gating | Apply blocked when required validation fails; apply also blocked when no validations were run (empty summary); informational validation does not block |
| 10 | A realistic porting task completes autonomously | §Autonomy Acceptance | End-to-end task (e.g., port a Python module) completes without human intervention |
| 11 | `agents automation-profile add/list/show` functional | §CLI Commands — automation-profile | Round-trip: add profile, list shows it, show displays details |
| 12 | `agents plan guard` shows active guards for a plan | §CLI Commands — plan guard | Command shows denylist, budget caps, tool limits |
@@ -47135,7 +47148,7 @@ This section defines the ordered milestone plan for CleverAgents v3.x, mapping a
- **Guard evaluation order**: Denylist checked first (fast reject), then budget caps, then tool call limits.
- **Autonomy threshold**: `0.0` = always automatic; `1.0` = always manual; eight built-in profiles from `manual` to `full-auto`.
- **Subtree recomputation**: Only decisions with `depends_on` edges to the corrected decision are recomputed; others preserved.
- **Validation gating**: `required` validations block apply; `informational` validations log but do not block.
- **Validation gating**: `required` validations block apply; an empty validation summary (no validations ran) also blocks apply; `informational` validations log but do not block.
#### Definition of Done
-130
View File
@@ -279,140 +279,10 @@ Feature: Actor configuration uncovered lines coverage
When I build an actor configuration from blob {"provider": "only-provider"}
Then a ValueError should be raised containing "model is required"
Scenario: from_blob merges default and v2 and override options
When I build an actor configuration from structured blob with defaults and overrides:
"""
{
"blob": {
"provider": "cli-provider",
"model": "cli-model",
"options": {"user": "blob"},
"agents": {
"writer": {
"config": {
"options": {"v2": "v2-option"}
}
}
}
},
"default_options": {"base": "default"},
"option_overrides": {"user": "override", "extra": "added"}
}
"""
Then the actor configuration should have provider "cli-provider" and model "cli-model"
And the actor configuration options should equal {"base": "default", "v2": "v2-option", "user": "override", "extra": "added"}
Scenario: from_blob with None blob defaults to empty data
When I build actor config from None blob with provider and model overrides
Then the actor configuration should have provider "override-provider" and model "override-model"
# --- _extract_v2_actor: lines 275-307 ---
Scenario: v2 YAML actor config infers provider and model
Given an actor config file "v2.yaml" with content:
"""
cleveragents:
default_router: main_router
agents:
paper_writer:
type: llm
config:
provider: openai
model: gpt-4
unsafe: true
options:
temperature: 0.5
routes:
main_router:
type: stream
operators:
- type: map
params:
agent: paper_writer
publications:
- __output__
"""
When I parse the actor configuration from file "v2.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "openai" and model "gpt-4"
And the actor configuration unsafe flag should be true
And the actor configuration graph descriptor should include key "routes"
And the actor configuration graph descriptor should include key "agents"
And the actor configuration graph descriptor should include key "cleveragents"
And the actor configuration options should equal {"temperature": 0.5}
Scenario: v2 extraction includes merges and templates keys when present
Given an actor config file "v2_merges.yaml" with content:
"""
agents:
writer:
config:
provider: openai
model: gpt-4
merges:
combined:
type: join
templates:
default:
system_prompt: "hello"
"""
When I parse the actor configuration from file "v2_merges.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "openai" and model "gpt-4"
And the actor configuration graph descriptor should include key "merges"
And the actor configuration graph descriptor should include key "templates"
Scenario: v2 extraction handles agent entry without config block
Given an actor config file "v2_no_config.yaml" with content:
"""
provider: fallback-provider
model: fallback-model
agents:
first:
type: llm
"""
When I parse the actor configuration from file "v2_no_config.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "fallback-provider" and model "fallback-model"
# --- _extract_v2_options: lines 316-326 ---
Scenario: v2 extraction skips non-dict agent entries
Given an actor config file "invalid_v2.yaml" with content:
"""
provider: fallback-provider
model: fallback-model
agents:
first: not-a-dict
"""
When I parse the actor configuration from file "invalid_v2.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "fallback-provider" and model "fallback-model"
Scenario: v2 options extraction returns None for agents without options
Given an actor config file "v2_no_opts.yaml" with content:
"""
agents:
writer:
config:
provider: openai
model: gpt-4
"""
When I parse the actor configuration from file "v2_no_opts.yaml" with overrides:
"""
{}
"""
Then the actor configuration should have provider "openai" and model "gpt-4"
And the actor configuration options should equal {}
Scenario: from_blob reads provider_type and model_id aliases
When I build an actor configuration from blob {"provider_type": "aliased-provider", "model_id": "aliased-model"}
Then the actor configuration should have provider "aliased-provider" and model "aliased-model"
+3 -399
View File
@@ -15,6 +15,7 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
And the registered actor name should be "local/my-assistant"
And the registered actor should exist in the actor service
@tdd_issue @tdd_issue_4300
Scenario: registry.add() accepts spec-compliant actors: map with separate provider and model
When I add a spec-compliant YAML with actors map and separate provider model
Then the actor should be registered with provider "anthropic" and model "claude-3"
@@ -53,24 +54,13 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
Then the actor should be registered with provider "nested-provider" and model "top-level-model"
And the registered actor should exist in the actor service
# ── Graph descriptor preserved from nested config ──────────────────
Scenario: registry.add() preserves graph descriptor from nested spec-compliant config
When I add a spec-compliant YAML with actors map and combined actor field
Then the registered actor graph descriptor should contain key "actors"
# ── Top-level provider+model still picks up nested unsafe/graph ──────
# ── Top-level provider+model still picks up nested unsafe ───────────
Scenario: registry.add() with top-level provider and model detects nested unsafe flag
When I add a YAML with top-level provider and model and nested actors map with unsafe flag
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should be marked unsafe
Scenario: registry.add() with top-level provider and model detects nested graph descriptor
When I add a YAML with top-level provider and model and nested actors map with graph descriptor
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor graph descriptor should contain key "actors"
# ── Unsafe confirmation gate ───────────────────────────────────────
Scenario: registry.add() rejects unsafe actor without confirmation
@@ -94,12 +84,6 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should not be marked unsafe
# ── Missing provider/model still rejected for non-v3 YAML ─────────
Scenario: registry.add() rejects non-v3 YAML without any provider or model
When I attempt to add a YAML with no provider or model anywhere
Then a spec-yaml ValidationError should be raised containing "provider"
# ── update=True path ───────────────────────────────────────────────
Scenario: registry.add() with update=True overwrites an existing actor using spec-compliant YAML
@@ -139,366 +123,6 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
And the registered actor should be marked unsafe
And the registered actor should exist in the actor service
# ── Multi-actor YAML rejection ───────────────────────────────────
Scenario: registry.add() rejects multi-actor YAML with a ValidationError
When I attempt to add a multi-actor YAML with two actor entries
Then a spec-yaml ValidationError should be raised containing "single-actor"
Scenario: registry.add() rejects multi-actor YAML via agents: fallback when actors: is null
When I attempt to add a YAML with actors null and multi-entry agents map
Then a spec-yaml ValidationError should be raised containing "single-actor"
# ── _extract_v2_actor handles actors: key ──────────────────────────
Scenario: _extract_v2_actor extracts provider/model from actors: map
When I call _extract_v2_actor with an actors map containing combined actor field
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor extracts from actors: map with separate fields
When I call _extract_v2_actor with an actors map containing separate provider model
Then the spec-yaml extracted provider should be "anthropic"
And the spec-yaml extracted model should be "claude-3"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor prefers actors: key over agents: key
When I call _extract_v2_actor with both actors and agents maps
Then the spec-yaml extracted provider should be "actors-provider"
And the spec-yaml extracted model should be "actors-model"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor graph descriptor contains agent key matching the actor entry name
When I call _extract_v2_actor with an actors map containing combined actor field
Then the spec-yaml extracted graph descriptor should contain key "agent"
And the spec-yaml extracted graph descriptor agent value should be "my_assistant"
Scenario: _extract_v2_actor with actors map containing unsafe flag
When I call _extract_v2_actor with an actors map containing unsafe true
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be True
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor returns None for empty data
When I call _extract_v2_actor with an empty dict
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should be None
Scenario: _extract_v2_actor returns None for actors key with empty map
When I call _extract_v2_actor with actors key containing empty map
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should be None
Scenario: _extract_v2_actor returns None for actors key with None value
When I call _extract_v2_actor with actors key containing None value
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should be None
# ── _extract_v2_actor handles agents: key ─────────────────────────
Scenario: _extract_v2_actor returns graph descriptor with agents map_key
When I call _extract_v2_actor with an agents map containing combined actor field
Then the spec-yaml extracted graph descriptor should contain key "agents"
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_actor edge cases: non-dict / missing config ────────
Scenario: _extract_v2_actor returns None for non-dict first entry
When I call _extract_v2_actor with a non-dict first entry in actors map
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
Scenario: _extract_v2_actor returns None for dict entry missing config block
When I call _extract_v2_actor with a dict entry missing config block
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_actor edge case: actors: {} blocks agents: fallback ─
Scenario: _extract_v2_actor with empty actors dict blocks agents fallback
When I call _extract_v2_actor with empty actors dict and valid agents map
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_actor edge case: actors: [] (list type) ────────────
Scenario: _extract_v2_actor with actors as list returns None
When I call _extract_v2_actor with actors as a list
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should be None
And the spec-yaml extracted unsafe flag should be False
# ── _extract_v2_options handles actors: and agents: keys ───────────
Scenario: _extract_v2_options extracts options from actors: map
When I call _extract_v2_options with an actors map containing options
Then the spec-yaml extracted options should contain key "temperature" with value 0.7
Scenario: _extract_v2_options extracts options from agents: map
When I call _extract_v2_options with an agents map containing options
Then the spec-yaml extracted options should contain key "max_tokens" with value 1024
Scenario: _extract_v2_options returns None for actors key with empty map
When I call _extract_v2_options with actors key containing empty map
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for actors key with None value
When I call _extract_v2_options with actors key containing None value
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for actors key with list value
When I call _extract_v2_options with actors key containing list value
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None when config block has no options key
When I call _extract_v2_options with an actors map where config has no options key
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for non-dict first entry in actors map
When I call _extract_v2_options with a non-dict first entry in actors map
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options returns None for dict entry missing config block
When I call _extract_v2_options with a dict entry missing config block
Then the spec-yaml extracted options should be None
Scenario: _extract_v2_options prefers actors: key over agents: key
When I call _extract_v2_options with both actors and agents maps containing options
Then the spec-yaml extracted options should contain key "source" with value "actors"
# ── Unsafe coercion edge cases (unsafe coercion) ────────────────
Scenario: _extract_v2_actor treats unsafe: "no" as False (not truthy string)
When I call _extract_v2_actor with unsafe value "no"
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: "yes" as False (not truthy string)
When I call _extract_v2_actor with unsafe value "yes"
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: 1 (integer) as True
When I call _extract_v2_actor with unsafe value 1
Then the spec-yaml extracted unsafe flag should be True
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: registry.add() accepts actors map with unsafe: 1 (integer) and unsafe flag
When I add a YAML with actors map where unsafe is integer 1 and the unsafe flag set
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should be marked unsafe
And the registered actor should exist in the actor service
Scenario: _extract_v2_actor treats unsafe: 1.0 (float) as True
When I call _extract_v2_actor with unsafe value 1.0
Then the spec-yaml extracted unsafe flag should be True
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: 2 (integer > 1) as False
When I call _extract_v2_actor with unsafe value 2
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
Scenario: _extract_v2_actor treats unsafe: 0 (integer zero) as False
When I call _extract_v2_actor with unsafe value 0
Then the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4"
# ── Top-level unsafe: 1 (integer) through registry.add() ────────────
Scenario: registry.add() rejects top-level unsafe: 1 (integer) without confirmation
When I attempt to add a YAML with top-level unsafe integer 1 and no flag
Then a spec-yaml ValidationError should be raised containing "unsafe"
Scenario: registry.add() accepts top-level unsafe: 1 (integer) with unsafe flag
When I add a YAML with top-level unsafe integer 1 and the unsafe flag set
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should be marked unsafe
And the registered actor should exist in the actor service
# ── Top-level graph_descriptor key through registry.add() (T7) ──────
Scenario: registry.add() resolves graph descriptor from top-level graph_descriptor key
When I add a YAML with top-level provider model and graph_descriptor key
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor graph descriptor should contain key "workflow"
Scenario: _extract_v2_actor includes top-level routes key in graph descriptor
When I call _extract_v2_actor with an actors map and a top-level routes key
Then the spec-yaml extracted graph descriptor should contain key "routes"
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── Legacy graph key fallback (M3) ─────────────────────────────────
Scenario: registry.add() resolves graph descriptor from legacy top-level graph key
When I add a YAML with top-level provider model and legacy graph key
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor graph descriptor should contain key "workflow"
# ── Empty actors map through registry.add() (M4) ───────────────────
Scenario: registry.add() with empty actors map does not fall back to agents map for provider/model
When I attempt to add a YAML with empty actors map and valid agents map but no top-level provider
Then a spec-yaml ValidationError should be raised containing "provider"
# ── provider_type / model_id aliases in nested config (m1) ─────────
Scenario: _extract_v2_actor extracts provider from provider_type alias in nested config
When I call _extract_v2_actor with an actors map using provider_type alias
Then the spec-yaml extracted provider should be "alias-provider"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: _extract_v2_actor extracts model from model_id alias in nested config
When I call _extract_v2_actor with an actors map using model_id alias
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "alias-model"
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── _extract_v2_options with empty dict (m4) ────────────────────────
Scenario: _extract_v2_options returns None for empty dict input
When I call _extract_v2_options with an empty dict
Then the spec-yaml extracted options should be None
# ── compiled_metadata value assertion (m5) ──────────────────────────
Scenario: registry.add() forwards compiled_metadata with correct values
When I add a spec-compliant YAML with schema_version "2.0" and compiled_metadata
Then the registered actor compiled metadata key "key" should have value "val"
# ── Combined actor field edge cases ────────────────────────────────
Scenario: Combined actor field without slash is ignored
When I call _extract_v2_actor with an actors map where actor field has no slash
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should contain key "actors"
And the spec-yaml extracted unsafe flag should be False
Scenario: Combined actor field does not override explicit provider but fills missing model
When I call _extract_v2_actor with an actors map where both actor and provider exist
Then the spec-yaml extracted provider should be "explicit-provider"
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: Combined actor field does not override explicit model but fills missing provider
When I call _extract_v2_actor with an actors map where both actor and model exist
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "explicit-model"
And the spec-yaml extracted unsafe flag should be False
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── Combined actor field malformed input edge cases ────────────────
Scenario: Combined actor field with empty provider part yields no provider
When I call _extract_v2_actor with an actors map where actor field has empty provider
Then the spec-yaml extracted provider should be None
And the spec-yaml extracted model should be "gpt-4"
And the spec-yaml extracted graph descriptor should contain key "actors"
Scenario: Combined actor field with empty model part yields no model
When I call _extract_v2_actor with an actors map where actor field has empty model
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be None
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── Combined actor field with multiple slashes (L1) ────────────────
Scenario: Combined actor field with multiple slashes splits on first slash only
When I call _extract_v2_actor with an actors map where actor field has multiple slashes
Then the spec-yaml extracted provider should be "openai"
And the spec-yaml extracted model should be "gpt-4/extra"
And the spec-yaml extracted graph descriptor should contain key "actors"
# ── actors: null + valid agents: through registry.add() (L2) ───────
Scenario: registry.add() with actors: null falls back to valid agents: map
When I add a YAML with actors null and valid agents map
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should exist in the actor service
# ── Top-level provider_type / model_id aliases through registry.add() (T8) ──
Scenario: registry.add() accepts top-level provider_type alias
When I add a YAML with top-level provider_type alias and model
Then the actor should be registered with provider "alias-provider" and model "gpt-4"
And the registered actor should exist in the actor service
Scenario: registry.add() accepts top-level model_id alias
When I add a YAML with top-level provider and model_id alias
Then the actor should be registered with provider "openai" and model "alias-model"
And the registered actor should exist in the actor service
# ── Top-level unsafe string coercion through registry.add() (T9) ───
Scenario: registry.add() treats top-level unsafe: "yes" as not unsafe (no gate rejection)
When I add a YAML with top-level unsafe string "yes" and provider model
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should not be marked unsafe
Scenario: registry.add() treats top-level unsafe: "no" as not unsafe (no gate rejection)
When I add a YAML with top-level unsafe string "no" and provider model
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor should not be marked unsafe
# ── Nested options extraction through registry.add() (M1) ──────────
Scenario: registry.add() extracts and preserves nested config options
When I add a spec-compliant YAML with actors map and nested options
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor config blob should contain options key "temperature" with value 0.9
And the registered actor config blob should contain options key "max_tokens" with value 2000
Scenario: registry.add() merges nested and top-level options (nested base, top-level overrides)
When I add a YAML with both top-level and nested options
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor config blob should contain options key "temperature" with value 0.7
And the registered actor config blob should contain options key "max_tokens" with value 2000
And the registered actor config blob should contain options key "top_p" with value 0.95
Scenario: registry.add() with non-dict top-level options uses nested options
When I add a YAML with non-dict top-level options and nested options
Then the actor should be registered with provider "openai" and model "gpt-4"
And the registered actor config blob should contain options key "temperature" with value 0.9
Scenario: registry.add() sets source: "yaml" default in config_blob
When I add a spec-compliant YAML with actors map and combined actor field
Then the registered actor config blob should contain source "yaml"
# ── _extract_v2_options shallow copy mutation isolation (NIT-2) ─────
Scenario: _extract_v2_options returns a shallow copy that does not mutate the original blob
When I call _extract_v2_options and mutate the returned dict
Then the original blob options should be unmodified
# ── M4: update=True creates actor when it doesn't exist ────────────
Scenario: registry.add() with update=True creates actor when it doesn't exist
@@ -518,25 +142,5 @@ Feature: ActorRegistry.add() accepts spec-compliant actor YAML formats
When upsert_actor is configured to raise RuntimeError and I add a valid YAML
Then a RuntimeError should have been propagated from add
# ── M7: actors: false (boolean) blocks agents fallback ─────────────
Scenario: registry.add() with actors: false blocks agents fallback and raises provider error
When I attempt to add a YAML with actors false and valid agents map
Then a spec-yaml ValidationError should be raised containing "provider"
# ── M8: non-dict compiled_metadata causes Pydantic error ───────────
Scenario: registry.add() with non-dict compiled_metadata raises a Pydantic validation error
When I attempt to add a valid YAML with compiled_metadata as a non-dict string
Then a Pydantic validation error should be raised for compiled_metadata
# ── M9: provider: 0 (integer zero) falls through to provider_type ──
Scenario: registry.add() with provider: 0 falls through to provider_type fallback
When I attempt to add a YAML with provider integer 0 and no provider_type
Then a spec-yaml ValidationError should be raised containing "provider"
Scenario: registry.add() with provider: 0 and valid provider_type uses provider_type
When I add a YAML with provider integer 0 and a valid provider_type
Then the actor should be registered with provider "fallback-provider" and model "gpt-4"
And the registered actor should exist in the actor service
-32
View File
@@ -26,12 +26,6 @@ Feature: v3 Actor YAML schema support in CLI and registry
When I call ActorConfiguration.from_blob with the v3 blob
Then the configuration should have a graph_descriptor with type "graph"
Scenario: ActorConfiguration.from_blob falls through to v2 for legacy YAML
Given a v2 actor blob with agents and routes
When I call ActorConfiguration.from_blob with the v2 blob
Then the configuration should have provider "openai"
And the configuration should have model "gpt-4"
Scenario: ActorRegistry.add validates v3 LLM actor YAML
Given a mock actor service for v3 testing
And a v3 LLM actor YAML text
@@ -67,11 +61,6 @@ Feature: v3 Actor YAML schema support in CLI and registry
And the reactive config should have a graph route
And the graph route edges should use source and target keys
Scenario: v3 format detection returns false for v2 data
Given a v2 actor config dict with agents key
When I check if the config is v3 format
Then it should not be detected as v3
# M14: test update=True path
Scenario: ActorRegistry.add with update=True overwrites existing actor
Given a mock actor service for v3 testing
@@ -174,27 +163,6 @@ Feature: v3 Actor YAML schema support in CLI and registry
When I call ActorRegistry.add with a non-mapping YAML string
Then a ValidationError should be raised mentioning mapping
# Coverage: registry.py — _add_legacy v3 schema validation failure
Scenario: ActorRegistry._add_legacy rejects invalid v3 YAML via schema validation
Given a mock actor service for v3 testing
And a legacy v3 YAML text with invalid schema
When I call ActorRegistry.add with the legacy invalid v3 YAML
Then a ValidationError should be raised mentioning invalid v3 actor
# Coverage: registry.py — _add_legacy missing provider/model in non-v3 blob
Scenario: ActorRegistry._add_legacy rejects non-v3 blob missing provider and model
Given a mock actor service for v3 testing
And a legacy non-v3 YAML text missing provider and model
When I call ActorRegistry.add with the legacy non-v3 YAML
Then a ValidationError should be raised mentioning provider and model
# Coverage: registry.py — _add_legacy unsafe flag rejection
Scenario: ActorRegistry._add_legacy rejects unsafe legacy actor without allow_unsafe
Given a mock actor service for v3 testing
And a legacy actor YAML text marked unsafe
When I call ActorRegistry.add with the legacy unsafe YAML
Then a ValidationError should be raised mentioning unsafe
# Coverage: registry.py — upsert_actor v3 validation failure
Scenario: ActorRegistry.upsert_actor rejects invalid v3 config_blob
Given a mock actor service for v3 testing
+27 -27
View File
@@ -459,22 +459,6 @@ Feature: Consolidated Actor
Then the config options should include the overrides
Scenario: _extract_v2_actor parses v2 agents block
When I extract v2 actor from a v2-style config with agents block
Then the extracted provider and model should be set
And the graph descriptor should contain agent info
Scenario: _extract_v2_actor returns None for missing agents
When I extract v2 actor from a config without agents block
Then all extracted values should be None
Scenario: _extract_v2_options extracts options from v2 config
When I extract v2 options from a v2-style config
Then the extracted options should contain expected keys
Scenario: from_file loads and parses config from disk
Given a temporary JSON config file with provider and model
When I create an ActorConfiguration from the file
@@ -876,6 +860,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/test-actor
type: llm
description: A test actor
provider: openai
model: gpt-4
"""
@@ -888,6 +874,8 @@ Feature: Consolidated Actor
When I add actor from YAML text with schema_version "2.0":
"""
name: local/versioned
type: llm
description: A versioned actor
provider: anthropic
model: claude-3
"""
@@ -899,6 +887,8 @@ Feature: Consolidated Actor
When I add actor from YAML text with compiled_metadata:
"""
name: local/compiled
type: llm
description: A compiled actor
provider: openai
model: gpt-4o
"""
@@ -910,12 +900,16 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/updatable
type: llm
description: An updatable actor
provider: openai
model: gpt-4
"""
And I update actor from YAML text:
"""
name: local/updatable
type: llm
description: An updatable actor
provider: openai
model: gpt-4-turbo
"""
@@ -927,6 +921,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/removable
type: llm
description: A removable actor
provider: openai
model: gpt-4
"""
@@ -939,12 +935,16 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/alpha
type: llm
description: An alpha actor
provider: openai
model: gpt-4
"""
And I add actor from YAML text with update:
"""
name: local/beta
type: llm
description: A beta actor
provider: anthropic
model: claude-3
"""
@@ -957,6 +957,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/auto-prefixed
type: llm
description: An auto-prefixed actor
provider: openai
model: gpt-4
"""
@@ -968,6 +970,8 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/fetchable
type: llm
description: A fetchable actor
provider: openai
model: gpt-4
"""
@@ -978,6 +982,8 @@ Feature: Consolidated Actor
Given an actor registry with no configured providers for persistence
When I attempt to add actor from YAML text without name:
"""
type: llm
description: No name actor
provider: openai
model: gpt-4
"""
@@ -989,12 +995,16 @@ Feature: Consolidated Actor
When I add actor from YAML text:
"""
name: local/duplicate
type: llm
description: A duplicate actor
provider: openai
model: gpt-4
"""
And I attempt to add duplicate actor from YAML text:
"""
name: local/duplicate
type: llm
description: A duplicate actor
provider: openai
model: gpt-4-turbo
"""
@@ -1008,17 +1018,7 @@ Feature: Consolidated Actor
And the actor "local/legacy-yaml" should have schema_version "1.5"
Scenario: Default schema version is applied when not specified
Given an actor registry with no configured providers for persistence
When I add actor from YAML text:
"""
name: local/default-version
provider: openai
model: gpt-4
"""
Then the actor "local/default-version" should have schema_version "1.0"
# ============================================================
# Originally from: actor_runtime.feature
# Feature: Tool-Calling Actor Runtime
+3 -3
View File
@@ -107,12 +107,12 @@ Feature: Plan explain and decision tree CLI commands
# plan tree - json format
# ------------------------------------------------------------------
@tdd_issue @tdd_issue_4254 @tdd_expected_fail
@tdd_issue @tdd_issue_4254
Scenario: Tree with json format
Given a set of test decisions forming a tree
When I format the tree as json
Then the json tree output should be valid json
And the json tree output should contain "decision_id"
Then the json tree output should be a valid json envelope
And the json tree output should contain "command"
# ------------------------------------------------------------------
# plan tree - yaml format
+2 -2
View File
@@ -65,13 +65,13 @@ Feature: Plan explain and tree CLI command coverage
Given pec a mock DecisionService returning a list of decisions
When pec I invoke "tree" with format "json"
Then pec the exit code should be 0
And pec the output should be valid json list
And pec the output should be valid json envelope
Scenario: Tree CLI renders yaml format
Given pec a mock DecisionService returning a list of decisions
When pec I invoke "tree" with format "yaml"
Then pec the exit code should be 0
And pec the output should contain "decision_id:"
And pec the output should contain "command:"
Scenario: Tree CLI renders table format
Given pec a mock DecisionService returning a list of decisions
@@ -0,0 +1,58 @@
@mock_only
Feature: PR Compliance Checklist in Implementation Pool Supervisor
As a pool supervisor
I want to pass a mandatory PR compliance checklist to every worker prompt
So that implementation workers complete all required items before creating a PR and avoid systemic merge blockers
Background:
Given the implementation-pool-supervisor.md agent definition exists
Scenario: Pool supervisor worker prompt includes the PR compliance checklist
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes the PR compliance checklist section
And Pool: the checklist is marked as MANDATORY
Scenario: Checklist item 1 — CHANGELOG.md update required
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a CHANGELOG.md checklist item
And Pool: the item instructs workers to add an entry under the Unreleased section
Scenario: Checklist item 2 — CONTRIBUTORS.md update required
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a CONTRIBUTORS.md checklist item
And Pool: the item instructs workers to add or update their contribution entry
Scenario: Checklist item 3 — commit footer required
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a commit footer checklist item
And Pool: the item specifies the ISSUES CLOSED footer format
Scenario: Checklist item 4 — CI must pass before PR creation
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a CI passes checklist item
And Pool: the item instructs workers to verify all quality gates are green
Scenario: Checklist item 5 — BDD/Behave tests required
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a BDD tests checklist item
And Pool: the item instructs workers to add or update Behave feature files
Scenario: Checklist item 6 — Epic reference required in PR description
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes an Epic reference checklist item
And Pool: the item instructs workers to reference the parent Epic issue number
Scenario: Checklist item 7 — Labels must be applied
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a labels checklist item
And Pool: the item instructs workers to apply labels via forgejo-label-manager
Scenario: Checklist item 8 — Milestone must be assigned
When I read the pool supervisor agent definition
Then Pool: worker prompt body includes a milestone checklist item
And Pool: the item instructs workers to assign the earliest open milestone
Scenario: All 8 checklist items are present in the worker prompt
When I read the pool supervisor agent definition
Then Pool: worker prompt body contains all 8 mandatory checklist items
@@ -0,0 +1,23 @@
@tdd_issue @tdd_issue_9055
Feature: PyYAML is a declared project dependency with minimum secure version
As a CleverAgents developer
I want the PyYAML dependency to be listed in pyproject.toml with a minimum secure version
So that vulnerable older versions of PyYAML cannot be installed
Background:
Given the pyproject.toml file exists at "pyproject.toml"
@tdd_issue @tdd_issue_9055
Scenario: PyYAML is listed in project dependencies with minimum version constraint
When I read the project dependencies from pyproject.toml
Then the dependency list should include a package matching "pyyaml>="
@tdd_issue @tdd_issue_9055
Scenario: PyYAML minimum version is >=6.0.3
When I read the PyYAML dependency specification from pyproject.toml
Then the minimum version should be at least "6.0.3"
@tdd_issue @tdd_issue_9055
Scenario: yaml module is importable as a project dependency
When I attempt to import the "yaml" module
Then the import should succeed without errors
@@ -17,11 +17,6 @@ from cleveragents.actor.yaml_loader import (
interpolate_env_vars,
load_yaml_text,
)
from cleveragents.actor.yaml_loader import (
_restore_template_syntax,
interpolate_env_vars,
load_yaml_text,
)
@then('an actor config ValueError should mention "{text}"')
@@ -314,76 +309,6 @@ def step_assert_options(context: Context) -> None:
assert context.actor_cfg.options["temperature"] == 0.9
@when("I extract v2 actor from a v2-style config with agents block")
def step_extract_v2_actor(context: Context) -> None:
data: dict[str, Any] = {
"agents": {
"main_agent": {
"config": {
"provider": "anthropic",
"model": "claude-3",
"unsafe": True,
"options": {"max_tokens": 1000},
}
}
},
"routes": [{"from": "main_agent", "to": "end"}],
}
context.extracted = ActorConfiguration._extract_v2_actor(data)
@then("the extracted provider and model should be set")
def step_assert_extracted(context: Context) -> None:
provider, model, _graph, unsafe = context.extracted
assert provider == "anthropic"
assert model == "claude-3"
assert unsafe is True
@then("the graph descriptor should contain agent info")
def step_assert_graph_descriptor(context: Context) -> None:
_, _, graph, _ = context.extracted
assert graph is not None
assert "agent" in graph
assert "routes" in graph
@when("I extract v2 actor from a config without agents block")
def step_extract_v2_no_agents(context: Context) -> None:
context.extracted = ActorConfiguration._extract_v2_actor({"provider": "openai"})
@then("all extracted values should be None")
def step_assert_all_none(context: Context) -> None:
provider, model, graph, unsafe = context.extracted
assert provider is None
assert model is None
assert graph is None
assert unsafe is False
@when("I extract v2 options from a v2-style config")
def step_extract_v2_options(context: Context) -> None:
data: dict[str, Any] = {
"agents": {
"main_agent": {
"config": {
"provider": "openai",
"model": "gpt-4",
"options": {"temperature": 0.7, "max_tokens": 500},
}
}
}
}
context.options = ActorConfiguration._extract_v2_options(data)
@then("the extracted options should contain expected keys")
def step_assert_extracted_options(context: Context) -> None:
assert context.options is not None
assert "temperature" in context.options
@when("I create an ActorConfiguration from the file")
def step_from_file(context: Context) -> None:
context.actor_cfg = ActorConfiguration.from_file(
File diff suppressed because it is too large Load Diff
@@ -525,88 +525,6 @@ def step_validation_error_mapping(context: Any) -> None:
assert "mapping" in msg, f"Error should mention 'mapping': {context.add_error}"
@given("a legacy v3 YAML text with invalid schema")
def step_legacy_v3_yaml_invalid_schema(context: Any) -> None:
# is_v3_yaml detects version starting with "3" or non-null type field.
# Provide a blob that triggers the v3 validation path but fails schema.
context.legacy_v3_yaml_invalid = yaml.safe_dump(
{
"name": "local/bad-v3",
"version": "3.0",
"type": "invalid_type_value", # not llm/graph/tool — fails ActorConfigSchema
"provider": "openai",
"model": "gpt-4",
}
)
@when("I call ActorRegistry.add with the legacy invalid v3 YAML")
def step_call_registry_add_legacy_invalid_v3(context: Any) -> None:
try:
context.registry.add(context.legacy_v3_yaml_invalid)
context.add_error = None
except ValidationError as exc:
context.add_error = exc
@then("a ValidationError should be raised mentioning invalid v3 actor")
def step_validation_error_invalid_v3(context: Any) -> None:
assert context.add_error is not None, "Expected a ValidationError to be raised"
msg = str(context.add_error).lower()
assert "invalid" in msg or "v3" in msg or "validation" in msg, (
f"Error should mention invalid v3 actor: {context.add_error}"
)
@given("a legacy non-v3 YAML text missing provider and model")
def step_legacy_non_v3_yaml_missing_fields(context: Any) -> None:
context.legacy_non_v3_yaml = yaml.safe_dump(
{
"name": "local/no-provider",
# no type field → not v3, no provider/model → should fail
}
)
@when("I call ActorRegistry.add with the legacy non-v3 YAML")
def step_call_registry_add_legacy_non_v3(context: Any) -> None:
try:
context.registry.add(context.legacy_non_v3_yaml)
context.add_error = None
except ValidationError as exc:
context.add_error = exc
@then("a ValidationError should be raised mentioning provider and model")
def step_validation_error_provider_model(context: Any) -> None:
assert context.add_error is not None, "Expected a ValidationError to be raised"
msg = str(context.add_error).lower()
assert "provider" in msg or "model" in msg, (
f"Error should mention 'provider' or 'model': {context.add_error}"
)
@given("a legacy actor YAML text marked unsafe")
def step_legacy_actor_yaml_unsafe(context: Any) -> None:
context.legacy_unsafe_yaml = yaml.safe_dump(
{
"name": "local/legacy-unsafe",
"provider": "openai",
"model": "gpt-4",
"unsafe": True,
}
)
@when("I call ActorRegistry.add with the legacy unsafe YAML")
def step_call_registry_add_legacy_unsafe(context: Any) -> None:
try:
context.registry.add(context.legacy_unsafe_yaml)
context.add_error = None
except ValidationError as exc:
context.add_error = exc
@when("I call ActorRegistry.upsert_actor with an invalid v3 config_blob")
def step_call_registry_upsert_invalid_v3(context: Any) -> None:
# Provide a blob that triggers is_v3_yaml() but fails ActorConfigSchema.
@@ -629,6 +547,15 @@ def step_call_registry_upsert_invalid_v3(context: Any) -> None:
context.add_error = exc
@then("a ValidationError should be raised mentioning invalid v3 actor")
def step_validation_error_invalid_v3(context: Any) -> None:
assert context.add_error is not None, "Expected a ValidationError to be raised"
msg = str(context.add_error).lower()
assert "invalid" in msg or "v3" in msg or "validation" in msg, (
f"Error should mention invalid v3 actor: {context.add_error}"
)
# ── Coverage gap: config_parser.py ───────────────────────────────────────
-41
View File
@@ -93,22 +93,6 @@ def step_v3_graph_blob(context: Any) -> None:
}
@given("a v2 actor blob with agents and routes")
def step_v2_blob(context: Any) -> None:
context.v2_blob = {
"agents": {
"main": {
"type": "llm",
"config": {
"provider": "openai",
"model": "gpt-4",
},
}
},
"routes": {},
}
@given("a mock actor service for v3 testing")
def step_mock_actor_service(context: Any) -> None:
mock_actor_service = MagicMock()
@@ -247,13 +231,6 @@ def step_v3_graph_config_dict(context: Any) -> None:
}
@given("a v2 actor config dict with agents key")
def step_v2_config_dict(context: Any) -> None:
context.v2_config = {
"agents": {"main": {"type": "llm", "config": {"provider": "openai"}}},
}
@given("an existing actor in the mock service")
def step_existing_actor(context: Any) -> None:
"""Set up mock to return an existing actor for duplicate checks."""
@@ -274,14 +251,6 @@ def step_call_from_blob_v3(context: Any) -> None:
)
@when("I call ActorConfiguration.from_blob with the v2 blob")
def step_call_from_blob_v2(context: Any) -> None:
context.actor_config = ActorConfiguration.from_blob(
blob=context.v2_blob,
name="local/test",
)
@when("I call ActorRegistry.add with the v3 YAML")
def step_call_registry_add_v3(context: Any) -> None:
context.result_actor = context.registry.add(context.v3_yaml_text)
@@ -329,11 +298,6 @@ def step_parse_v3_config(context: Any) -> None:
context.reactive_config = parser._build(context.v3_config)
@when("I check if the config is v3 format")
def step_check_v3_format(context: Any) -> None:
context.is_v3 = ReactiveConfigParser._is_v3_format(context.v2_config)
# ── Then steps ───────────────────────────────────────────────────────────
@@ -470,11 +434,6 @@ def step_reactive_has_graph_route(context: Any) -> None:
assert len(route.edges) >= 1, f"Expected at least 1 edge, got {len(route.edges)}"
@then("it should not be detected as v3")
def step_not_v3(context: Any) -> None:
assert context.is_v3 is False, "Expected v2 data to NOT be detected as v3"
@then("the graph route edges should use source and target keys")
def step_graph_edges_use_source_target(context: Any) -> None:
routes = context.reactive_config.routes
@@ -807,6 +807,22 @@ def step_pec_output_valid_json_list(context: Context) -> None:
assert isinstance(data, list), "Expected JSON array"
@then("pec the output should be valid json envelope")
def step_pec_output_valid_json_envelope(context: Context) -> None:
parsed = json.loads(context.pec_result.output.strip())
assert isinstance(parsed, dict), f"Expected JSON object, got {type(parsed)}"
assert _ENVELOPE_KEYS.issubset(parsed.keys()), (
f"Expected envelope keys {_ENVELOPE_KEYS}, got {set(parsed.keys())}"
)
data = parsed["data"]
assert isinstance(data, dict), (
f"Expected envelope data to be a dict, got {type(data)}"
)
assert "plan_id" in data, (
f"Expected 'plan_id' in envelope data, got {set(data.keys())}"
)
@then("pec the tree should exclude the superseded grandchild")
def step_pec_tree_excludes_orphan(context: Context) -> None:
# Tree should have exactly one root with one child and zero grandchildren.
+12
View File
@@ -405,6 +405,18 @@ def step_tree_json_valid(context: Context) -> None:
assert isinstance(parsed, list), "Expected a JSON array"
@then("the json tree output should be a valid json envelope")
def step_tree_json_valid_envelope(context: Context) -> None:
parsed = json.loads(context.pe_tree_json)
assert isinstance(parsed, dict), (
f"Expected a JSON object (envelope), got {type(parsed)}"
)
_envelope_keys = {"command", "status", "exit_code", "data", "timing", "messages"}
assert _envelope_keys.issubset(parsed.keys()), (
f"Expected envelope keys {_envelope_keys}, got {set(parsed.keys())}"
)
@then('the json tree output should contain "{text}"')
def step_tree_json_contains(context: Context, text: str) -> None:
assert text in context.pe_tree_json, f"Expected '{text}' in tree JSON"
@@ -0,0 +1,219 @@
"""Step definitions for PR compliance checklist in implementation pool supervisor.
This file uses parameterized @then decorators with unique step text that
distinguishes pool-supervisor checks from the shared compliance checklist
steps in pr_compliance_checklist_steps.py, preventing Behave AmbiguousStep
errors when both feature files are run together.
Each validator is imported from the shared pr_compliance_checklist_steps module's
validation logic (via the _verify module) to avoid code duplication while using
unique step text prefixes ("Pool:") for disambiguation.
"""
from collections.abc import Callable
from pathlib import Path
from typing import Any
from behave import given, then, when
PROJECT_ROOT = Path(__file__).resolve().parents[2]
AGENT_DEF_PATH = (
PROJECT_ROOT / ".opencode" / "agents" / "implementation-pool-supervisor.md"
)
# ---------------------------------------------------------------------------
# Shared validation helpers — identical logic to pr_compliance_checklist_steps.py
# ---------------------------------------------------------------------------
_required_items = [
"CHANGELOG.md",
"CONTRIBUTORS.md",
"ISSUES CLOSED",
"CI passes",
"BDD/Behave tests",
"Epic reference",
"forgejo-label-manager",
"earliest open milestone",
]
VALIDATORS: dict[str, Callable[[str], bool]] = {
"includes checklist section": lambda c: "PR Compliance Checklist" in c,
"is marked MANDATORY": lambda c: "MANDATORY" in c,
"has CHANGELOG.md item": lambda c: "CHANGELOG.md" in c,
"references Unreleased": lambda c: "[Unreleased]" in c,
"has CONTRIBUTORS.md item": lambda c: "CONTRIBUTORS.md" in c,
"instructs add or update": lambda c: "add or update" in c,
"has commit footer item": lambda c: "Commit footer" in c,
"specifies ISSUES CLOSED": lambda c: "ISSUES CLOSED" in c,
"has CI passes item": lambda c: "CI passes" in c,
"mentions quality gates": lambda c: "quality gates" in c,
"has BDD tests item": lambda c: "BDD/Behave tests" in c,
"instructs add or update features": lambda c: "added or updated" in c,
"has Epic reference item": lambda c: "Epic reference" in c,
"references parent Epic": lambda c: "parent Epic" in c,
"has labels item": lambda c: "Labels" in c,
"mentions forgejo-label-manager": lambda c: "forgejo-label-manager" in c,
"has milestone item": lambda c: "Milestone" in c,
"earliest open milestone": lambda c: "earliest open milestone" in c,
"all 8 items present": lambda c: all(item in c for item in _required_items),
}
def _make_validator(key: str) -> Callable[[Any], None]:
"""Factory that creates a typed Behave validator from a shared helper."""
def validator(context: Any) -> None:
content = context.agent_def_content
check_fn = VALIDATORS.get(key)
assert check_fn(content), f"Pool supervisor agent definition failed: {key}"
return validator
# ---------------------------------------------------------------------------
# Unique @given and @when — scoped to the pool supervisor agent def only
# ---------------------------------------------------------------------------
@given("the implementation-pool-supervisor.md agent definition exists")
def step_agent_def_exists(context: Any) -> None:
"""Verify the pool supervisor agent definition file exists."""
assert AGENT_DEF_PATH.exists(), f"Agent definition not found at {AGENT_DEF_PATH}"
context.agent_def_path = AGENT_DEF_PATH
@when("I read the pool supervisor agent definition")
def step_read_agent_def(context: Any) -> None:
"""Read the pool supervisor agent definition."""
context.agent_def_content = AGENT_DEF_PATH.read_text(encoding="utf-8")
# ---------------------------------------------------------------------------
# Unique @then — prefixed with "Pool:" so they never conflict with the
# shared pr_compliance_checklist_steps.py step definitions.
# ---------------------------------------------------------------------------
# Scenario: Pool supervisor worker prompt includes the PR compliance checklist
@then("Pool: worker prompt body includes the PR compliance checklist section")
def pool_step_prompt_includes_checklist(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes the PR compliance checklist."""
_make_validator("includes checklist section")(context)
@then("Pool: the checklist is marked as MANDATORY")
def pool_step_checklist_is_mandatory(context: Any) -> None:
"""Verify the pool-supervisor checklist is marked as MANDATORY."""
_make_validator("is marked MANDATORY")(context)
# Scenario: Checklist item 1 — CHANGELOG.md update required
@then("Pool: worker prompt body includes a CHANGELOG.md checklist item")
def pool_step_prompt_includes_changelog_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a CHANGELOG.md checklist item."""
_make_validator("has CHANGELOG.md item")(context)
@then("Pool: the item instructs workers to add an entry under the Unreleased section")
def pool_step_changelog_item_unreleased(context: Any) -> None:
"""Verify the CHANGELOG.md item mentions the Unreleased section."""
_make_validator("references Unreleased")(context)
# Scenario: Checklist item 2 — CONTRIBUTORS.md update required
@then("Pool: worker prompt body includes a CONTRIBUTORS.md checklist item")
def pool_step_prompt_includes_contributors_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a CONTRIBUTORS.md checklist item."""
_make_validator("has CONTRIBUTORS.md item")(context)
@then("Pool: the item instructs workers to add or update their contribution entry")
def pool_step_contributors_item_add_update(context: Any) -> None:
"""Verify the CONTRIBUTORS.md item instructs workers to add or update."""
_make_validator("instructs add or update")(context)
# Scenario: Checklist item 3 — commit footer required
@then("Pool: worker prompt body includes a commit footer checklist item")
def pool_step_prompt_includes_commit_footer_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a commit footer checklist item."""
_make_validator("has commit footer item")(context)
@then("Pool: the item specifies the ISSUES CLOSED footer format")
def pool_step_commit_footer_issues_closed(context: Any) -> None:
"""Verify the commit footer item specifies the ISSUES CLOSED format."""
_make_validator("specifies ISSUES CLOSED")(context)
# Scenario: Checklist item 4 — CI must pass before PR creation
@then("Pool: worker prompt body includes a CI passes checklist item")
def pool_step_prompt_includes_ci_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a CI passes checklist item."""
_make_validator("has CI passes item")(context)
@then("Pool: the item instructs workers to verify all quality gates are green")
def pool_step_ci_item_quality_gates(context: Any) -> None:
"""Verify the CI item instructs workers to verify quality gates are green."""
_make_validator("mentions quality gates")(context)
# Scenario: Checklist item 5 — BDD/Behave tests required
@then("Pool: worker prompt body includes a BDD tests checklist item")
def pool_step_prompt_includes_bdd_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a BDD/Behave tests checklist item."""
_make_validator("has BDD tests item")(context)
@then("Pool: the item instructs workers to add or update Behave feature files")
def pool_step_bdd_item_feature_files(context: Any) -> None:
"""Verify the BDD item instructs workers to add or update feature files."""
_make_validator("instructs add or update features")(context)
# Scenario: Checklist item 6 — Epic reference required in PR description
@then("Pool: worker prompt body includes an Epic reference checklist item")
def pool_step_prompt_includes_epic_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes an Epic reference checklist item."""
_make_validator("has Epic reference item")(context)
@then("Pool: the item instructs workers to reference the parent Epic issue number")
def pool_step_epic_item_parent_reference(context: Any) -> None:
"""Verify the Epic item instructs workers to reference the parent Epic."""
_make_validator("references parent Epic")(context)
# Scenario: Checklist item 7 — Labels must be applied
@then("Pool: worker prompt body includes a labels checklist item")
def pool_step_prompt_includes_labels_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a labels checklist item."""
_make_validator("has labels item")(context)
@then("Pool: the item instructs workers to apply labels via forgejo-label-manager")
def pool_step_labels_item_forgejo_label_manager(context: Any) -> None:
"""Verify the labels item instructs workers to use forgejo-label-manager."""
_make_validator("mentions forgejo-label-manager")(context)
# Scenario: Checklist item 8 — Milestone must be assigned
@then("Pool: worker prompt body includes a milestone checklist item")
def pool_step_prompt_includes_milestone_item(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body includes a milestone checklist item."""
_make_validator("has milestone item")(context)
@then("Pool: the item instructs workers to assign the earliest open milestone")
def pool_step_milestone_item_earliest(context: Any) -> None:
"""Verify the milestone item instructs workers to assign the earliest open milestone."""
_make_validator("earliest open milestone")(context)
# Scenario: All 8 checklist items are present in the worker prompt
@then("Pool: worker prompt body contains all 8 mandatory checklist items")
def pool_step_prompt_contains_all_8_items(context: Any) -> None:
"""Verify the pool-supervisor worker prompt body contains all 8 mandatory checklist items."""
_make_validator("all 8 items present")(context)
@@ -0,0 +1,108 @@
"""Step definitions for PyYAML security dependency verification (#9055).
Note: the shared steps for reading pyproject.toml and verifying importability
are defined in ``tdd_a2a_sdk_dependency_steps.py`` to avoid AmbiguousStep
conflicts. This file contains only the PyYAML-specific step definitions.
"""
from __future__ import annotations
import re
from typing import Any
from behave import then, when
from behave.runner import Context
from packaging.version import parse as parse_version
@then('the dependency list should include a package matching "{pattern}"')
def step_dependency_matches_pattern(context: Context, pattern: str) -> None:
"""Assert that a dependency in the project dependencies matches the given regex pattern."""
deps: list[str] = context.project_dependencies
found = any(re.search(pattern, dep) for dep in deps)
assert found, (
f"No dependency matching '{pattern}' found in "
f"[project.dependencies]. Current deps: {deps}"
)
@when("I read the PyYAML dependency specification from pyproject.toml")
def step_read_pyyaml_dependency(context: Context) -> None:
"""Read and parse the PyYAML dependency from pyproject.toml."""
content = context.pyproject_path.read_bytes()
data: dict[str, Any] = _load_toml(content)
deps: list[str] = data.get("project", {}).get("dependencies", [])
# Find the pyyaml dependency entry
pyyaml_dep = None
for dep in deps:
if dep.strip().startswith("pyyaml"):
pyyaml_dep = dep.strip()
break
assert pyyaml_dep is not None, (
f"PyYAML dependency not found in [project.dependencies]. Current deps: {deps}"
)
# Parse the minimum version from the constraint
# Examples: "pyyaml>=6.0.3", "pyyaml>=6.0", "pyyaml==6.0.2"
match = re.search(r"(?:>=|==|~>)([\d]+(?:\.[\d]+)*(?:\.\w+)*)", pyyaml_dep)
assert match is not None, (
f"Could not parse version from PyYAML dependency: {pyyaml_dep}"
)
context.pyyaml_version_spec = pyyaml_dep
context.pyyaml_min_version = match.group(1)
@then('the minimum version should be at least "{expected}"')
def step_pyyaml_minimum_version(context: Context, expected: str) -> None:
"""Assert that the PyYAML minimum version is >= expected version."""
min_version = context.pyyaml_min_version
assert parse_version(min_version) >= parse_version(expected), (
f"PyYAML minimum version {min_version} is less than required {expected}. "
f"Dependency spec: {context.pyyaml_version_spec}"
)
def _flatten_toml_dict(d: dict[Any, Any]) -> dict[str, Any]:
"""Recursively convert a tomlkit dict to a plain Python dict.
tomlkit wraps every value in a proxy object that behaves like the
underlying Python type but is not identical to it. This helper
strips those wrappers so the result is a plain ``dict[str, Any]``
compatible with ``tomllib`` output.
"""
result: dict[str, Any] = {}
for key, value in d.items():
if isinstance(value, dict):
result[key] = _flatten_toml_dict(value)
else:
result[key] = value
return result
def _toml_to_python(data: object) -> dict[str, Any]:
"""Convert a tomlkit document to a plain Python dict.
Delegates recursive flattening to the module-level
``_flatten_toml_dict`` helper so the logic stays testable in
isolation and ruff does not flag nested function definitions.
"""
if isinstance(data, dict):
return _flatten_toml_dict(data)
raise TypeError(f"Expected dict-like TOML table, got {type(data).__name__}")
def _load_toml(content: bytes) -> dict[str, Any]:
"""Load a TOML file from bytes. Uses tomllib if available, else fallback."""
try:
import tomllib
return tomllib.loads(content.decode("utf-8"))
except ImportError: # pragma: no cover — Python <3.11
# Fallback for older Python versions without tomllib
import tomlkit
parsed_doc = tomlkit.parse(content.decode("utf-8"))
return _toml_to_python(parsed_doc)
+1
View File
@@ -47,6 +47,7 @@ dependencies = [
"tomlkit>=0.13.0", # TOML writing with comment preservation for config CLI
"tenacity>=8.2.0", # Retry framework for service layer resilience
"aiohttp>=3.13.4", # CVE-2026-34515 mitigation: open redirect vulnerability
"pyyaml>=6.0.3", # Security: address known YAML parsing vulnerabilities
"a2a-sdk>=0.3.0,<1.0.0", # A2A Python SDK — required transport for local (stdio) and server (HTTP) modes (ADR-047); pinned <1.0.0 (removed legacy A2AClient)
]
-34
View File
@@ -4,40 +4,6 @@ Library Collections
Library Process
*** Test Cases ***
V2 Actor Config Produces Provider And Graph Descriptor
${config}= Set Variable ${OUTPUT DIR}/v2_actor.yaml
${content}= Catenate SEPARATOR=\n
... cleveragents:
... ${SPACE*2}default_router: main_router
... agents:
... ${SPACE*2}paper_writer:
... ${SPACE*4}type: llm
... ${SPACE*4}config:
... ${SPACE*6}provider: openai
... ${SPACE*6}model: gpt-4
... ${SPACE*6}unsafe: true
... ${SPACE*6}options:
... ${SPACE*8}temperature: 0.5
... routes:
... ${SPACE*2}main_router:
... ${SPACE*4}type: stream
... ${SPACE*4}operators:
... ${SPACE*6}- type: map
... ${SPACE*8}params:
... ${SPACE*10}agent: paper_writer
... ${SPACE*4}publications:
... ${SPACE*6}- __output__
Create File ${config} ${content}
${result}= Run Process ${PYTHON} robot/helper_actor_config.py ${config}
Should Be Equal As Integers ${result.rc} 0
${payload}= Evaluate __import__('json').loads('''${result.stdout.strip()}''')
Should Be Equal ${payload['provider']} openai
Should Be Equal ${payload['model']} gpt-4
Should Be True ${payload['unsafe']}
List Should Contain Value ${payload['graph_keys']} agents
List Should Contain Value ${payload['graph_keys']} routes
Should Be Equal As Numbers ${payload['options']['temperature']} 0.5
Missing Config File Exits With Non-Zero Code And Stderr Message
[Tags] tdd_issue tdd_issue_4202 tdd_expected_fail
${missing}= Set Variable ${OUTPUT DIR}/does_not_exist_actor.yaml
+18 -5
View File
@@ -386,11 +386,24 @@ M6 E2E Hierarchical Decomposition Via Plan Tree
# P0-4: Hard assertion — tree command must succeed.
Should Be Equal As Integers ${tree.rc} 0 msg=plan tree failed (rc=${tree.rc}): ${tree.stderr}
Should Not Be Empty ${tree.stdout} Plan tree output should not be empty
# P0-4: Hard assertion — at least one decision node must exist after execution.
${decision_count}= Evaluate $tree.stdout.count('"decision_id"')
Log Decision tree contains ${decision_count} decision node(s)
Should Be True ${decision_count} >= 1
... Plan tree should contain at least one decision node after execution (found ${decision_count})
# P0-4: Hard assertion — tree output must contain the spec-required envelope.
# The new envelope format wraps the tree in a command envelope with
# "command", "status", "exit_code", "data", "timing", "messages" keys.
# The "data" field contains "plan_id", "tree", "summary", "child_plans",
# and "decision_ids" (a mapping of human-readable keys to decision ULIDs).
${has_command_key}= Evaluate '"command"' in $tree.stdout
Should Be True ${has_command_key}
... Plan tree JSON output should contain spec-required envelope "command" key
${has_data_key}= Evaluate '"data"' in $tree.stdout
Should Be True ${has_data_key}
... Plan tree JSON output should contain spec-required envelope "data" key
${has_plan_id_key}= Evaluate '"plan_id"' in $tree.stdout
Should Be True ${has_plan_id_key}
... Plan tree JSON output should contain "plan_id" in envelope data
# Check for decision_ids mapping (proves at least one decision was recorded)
${has_decision_ids}= Evaluate '"decision_ids"' in $tree.stdout
Should Be True ${has_decision_ids}
... Plan tree should contain decision_ids mapping after execution
# Check for hierarchical children (decomposition infrastructure indicator)
${has_children_key}= Evaluate '"children"' in $tree.stdout
IF ${has_children_key}
+70 -194
View File
@@ -1,7 +1,7 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Literal, cast
from typing import Any, cast
from pydantic import BaseModel, Field
@@ -42,7 +42,7 @@ class ActorConfiguration(BaseModel):
@staticmethod
def load_blob_from_file(config_path: Path) -> dict[str, Any]:
"""Load a configuration blob from a JSON or YAML file (v2 parity)."""
"""Load a configuration blob from a JSON or YAML file."""
path = config_path.expanduser()
if not path.exists():
raise ValueError(f"Config file not found: {path}")
@@ -88,36 +88,25 @@ class ActorConfiguration(BaseModel):
) -> ActorConfiguration:
"""Coerce an incoming config blob plus CLI overrides into a validated model.
Supports both the legacy v2 ``agents/routes`` YAML layout and the v3
``ActorConfigSchema`` format (identified by a top-level ``type`` key
whose value is one of ``llm``, ``graph``, or ``tool``).
Supports the v3 ``ActorConfigSchema`` format (identified by a top-level
``type`` key whose value is one of ``llm``, ``graph``, or ``tool``).
Provider and model may be at the top level OR nested inside
``actors.<actor-name>.config``.
"""
data: dict[str, Any] = dict(blob) if isinstance(blob, dict) else {}
# Try v3 extraction first (presence of 'type' with a v3 value).
v3_provider, v3_model, v3_graph, v3_unsafe = cls._extract_v3_actor(data)
# Fall back to v2 extraction when v3 didn't match.
v2_provider, v2_model, v2_graph, v2_unsafe = cls._extract_v2_actor(data)
v2_options = cls._extract_v2_options(data)
resolved_provider = (
provider
or data.get("provider")
or data.get("provider_type")
or v3_provider
or v2_provider
)
resolved_model = (
model or data.get("model") or data.get("model_id") or v3_model or v2_model
provider or data.get("provider") or data.get("provider_type") or v3_provider
)
resolved_model = model or data.get("model") or data.get("model_id") or v3_model
resolved_graph = (
graph_descriptor
or data.get("graph_descriptor")
or data.get("graph")
or v3_graph
or v2_graph
)
options_raw = data.get("options")
@@ -127,8 +116,6 @@ class ActorConfiguration(BaseModel):
merged_options: dict[str, Any] = {}
if default_options:
merged_options.update(default_options)
if v2_options:
merged_options.update(v2_options)
if options_value:
merged_options.update(options_value)
if option_overrides:
@@ -136,10 +123,7 @@ class ActorConfiguration(BaseModel):
top_unsafe_raw = data.get("unsafe", False)
resolved_unsafe = (
(top_unsafe_raw is True or top_unsafe_raw == 1)
or v3_unsafe
or v2_unsafe
or unsafe
(top_unsafe_raw is True or top_unsafe_raw == 1) or v3_unsafe or unsafe
)
if not resolved_provider:
@@ -167,27 +151,6 @@ class ActorConfiguration(BaseModel):
unsafe=resolved_unsafe,
)
@staticmethod
@staticmethod
def _resolve_actors_map(
data: dict[str, Any],
) -> tuple[Any, Literal["actors", "agents"]]:
"""Resolve the actors/agents map from a parsed YAML blob.
The spec allows either ``actors:`` or ``agents:`` as the top-level
map key (oneOf). ``actors:`` is preferred (spec-canonical); the
``agents:`` key is only consulted when ``actors:`` is absent or
explicitly ``null``.
Returns ``(map_obj, map_key)`` where *map_obj* is the raw value
(may be ``None``, a non-dict, or a dict) and *map_key* is the
string ``"actors"`` or ``"agents"``.
"""
actors_val = data.get("actors")
if actors_val is not None:
return actors_val, "actors"
return data.get("agents"), "agents"
@staticmethod
def _extract_v3_actor(
data: dict[str, Any],
@@ -195,13 +158,13 @@ class ActorConfiguration(BaseModel):
"""Derive provider/model/graph from the v3 Actor YAML schema format.
The v3 schema is identified by a top-level ``type`` key whose value
is one of ``llm``, ``graph``, or ``tool``. The model is taken from
the top-level ``model`` key. Because v3 YAML does not carry a
``provider`` field the provider is inferred from the model string:
is one of ``llm``, ``graph``, or ``tool`` OR by a top-level
``actors:`` map containing at least one entry whose ``config:`` block
carries a ``type`` field with one of those values.
* If the model contains ``/`` the prefix is used as provider
(e.g. ``openai/gpt-4`` provider ``openai``).
* Otherwise ``"custom"`` is used as a fallback.
Provider and model may appear at the top level OR nested inside
``actors.<actor-name>.config``. When found in the nested config,
they take precedence over top-level values.
For ``type: graph`` actors the ``route`` block is wrapped into a
graph descriptor.
@@ -211,28 +174,65 @@ class ActorConfiguration(BaseModel):
may be ``None`` if the data does not look like v3.
"""
actor_type = data.get("type")
if not isinstance(actor_type, str):
return None, None, None, False
if actor_type.lower() not in _V3_ACTOR_TYPES:
return None, None, None, False
if not isinstance(actor_type, str) or actor_type.lower() not in _V3_ACTOR_TYPES:
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
nested_type = config_block.get("type")
is_v3_type = (
isinstance(nested_type, str)
and nested_type.lower() in _V3_ACTOR_TYPES
)
if is_v3_type:
actor_type = nested_type
break
break
if not isinstance(actor_type, str):
return None, None, None, False
model_value = data.get("model")
# C2: model is optional for TOOL actors — only reject when model
# is absent for types that require it (LLM, GRAPH).
if not model_value or not isinstance(model_value, str):
if actor_type.lower() != "tool":
return None, None, None, False
# TOOL actors may omit model; use empty string as sentinel.
model_value = ""
# Infer provider from model string or default to "custom".
provider_value = (
infer_provider_from_model(model_value) if model_value else "custom"
)
provider_value = data.get("provider")
unsafe_flag = bool(data.get("unsafe", False))
# Build a graph descriptor for graph actors from the route block.
if not model_value or not isinstance(model_value, str):
if actor_type.lower() != "tool":
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
mv = config_block.get("model")
if isinstance(mv, str) and mv:
model_value = mv
break
break
if not model_value:
if actor_type.lower() != "tool":
return None, None, None, False
model_value = ""
if not provider_value or not isinstance(provider_value, str):
actors_val = data.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, first_entry in actors_val.items():
if isinstance(first_entry, dict):
config_block = first_entry.get("config")
if isinstance(config_block, dict):
pv = config_block.get("provider")
if isinstance(pv, str) and pv:
provider_value = pv
break
break
if not provider_value:
provider_value = (
infer_provider_from_model(model_value) if model_value else "custom"
)
graph_descriptor: dict[str, Any] | None = None
if actor_type.lower() == "graph":
route_raw = data.get("route")
@@ -244,127 +244,3 @@ class ActorConfiguration(BaseModel):
}
return provider_value, model_value, graph_descriptor, unsafe_flag
@staticmethod
def _extract_v2_actor(
data: dict[str, Any],
) -> tuple[str | None, str | None, dict[str, Any] | None, bool]:
"""Derive provider/model/graph from the v2/spec YAML actor config format.
Supports both the legacy ``agents:`` map key and the spec-compliant
``actors:`` map key. Within each actor's ``config:`` block, the
combined ``actor`` field (e.g. ``"openai/gpt-4"``) is also accepted
as an alternative to separate ``provider`` + ``model`` fields.
"""
map_obj, map_key = ActorConfiguration._resolve_actors_map(data)
# NOTE: Any non-None value for ``actors:`` — including falsy values
# such as ``actors: {}``, ``actors: false``, ``actors: 0``, or
# ``actors: ""`` — blocks the ``agents:`` fallback. This is
# intentional: a present ``actors:`` key explicitly declares the
# actors map (even if empty or invalid), whereas ``actors: null``
# (or absent) defers to ``agents:``.
#
# NOTE: Only the **first** entry in the actors/agents map is inspected
# for provider, model, unsafe, and graph descriptor. The ``add()``
# method rejects multi-actor YAML (>1 entry) with a ``ValidationError``
# to prevent silent data loss. The multi-actor case is handled by the
# v3 schema validation path.
if isinstance(map_obj, dict) and map_obj:
map_dict = cast(dict[str, Any], map_obj)
# Only the first actor entry is used for provider/model extraction.
for actor_key, first_entry in map_dict.items():
if isinstance(first_entry, dict):
entry_dict = cast(dict[str, Any], first_entry)
config_block = entry_dict.get("config")
if isinstance(config_block, dict):
config_dict = cast(dict[str, Any], config_block)
# Support the combined ``actor`` field format
# (e.g. ``"openai/gpt-4"`` → provider + model).
# NOTE: ``provider_type`` and ``model_id`` are
# accepted as aliases for backward compatibility,
# even though the spec's JSON schema for the
# ``config:`` block only lists ``provider``,
# ``model``, and ``actor``.
# Use ``is not None`` (not ``or``) so that falsy
# values like ``""`` or ``0`` are preserved rather
# than falling through to the alias.
provider_value = config_dict.get("provider")
if provider_value is None:
provider_value = config_dict.get("provider_type")
model_value = config_dict.get("model")
if model_value is None:
model_value = config_dict.get("model_id")
combined_actor = config_dict.get("actor")
if (
combined_actor
and isinstance(combined_actor, str)
and "/" in combined_actor
):
parts = combined_actor.split("/", 1)
if not provider_value:
provider_value = parts[0]
if not model_value:
model_value = parts[1]
unsafe_raw = config_dict.get("unsafe", False)
# Accept only boolean ``True`` or integer ``1`` as
# unsafe. Note: ``unsafe_raw == 1`` also matches
# ``1.0`` (float) due to Python's numeric equality
# rules — this is fail-safe (treats 1.0 as unsafe).
unsafe_flag = unsafe_raw is True or unsafe_raw == 1
# The graph descriptor is always built and returned
# when a valid ``config:`` block is found, regardless
# of whether ``provider``/``model`` were extracted.
# This allows the caller to preserve the graph
# structure even when provider/model come from
# elsewhere (e.g. top-level fields).
descriptor: dict[str, Any] = {
"agent": actor_key,
map_key: dict(map_dict),
}
for key in (
"routes",
"merges",
"templates",
"cleveragents",
):
if key in data and data[key] is not None:
descriptor[key] = data[key]
return (
str(provider_value) if provider_value else None,
str(model_value) if model_value else None,
descriptor,
unsafe_flag,
)
break # first entry only
return None, None, None, False
@staticmethod
def _extract_v2_options(data: dict[str, Any]) -> dict[str, Any] | None:
"""Extract option defaults from the v2/spec actor config format.
Supports both ``actors:`` (spec-canonical) and ``agents:`` (legacy)
map keys.
"""
map_obj, _ = ActorConfiguration._resolve_actors_map(data)
if isinstance(map_obj, dict) and map_obj:
map_dict = cast(dict[str, Any], map_obj)
for _, first_entry in map_dict.items():
if isinstance(first_entry, dict):
entry_dict = cast(dict[str, Any], first_entry)
config_block = entry_dict.get("config")
if isinstance(config_block, dict):
config_dict = cast(dict[str, Any], config_block)
options = config_dict.get("options")
if isinstance(options, dict):
# Return a shallow copy so downstream mutations
# (e.g. ``config_blob["options"]`` updates) do
# not propagate back into the original blob.
return dict(cast(dict[str, Any], options))
break # first entry only
return None
-171
View File
@@ -1,171 +0,0 @@
"""Legacy (v2) actor registration logic extracted from ``ActorRegistry``.
This module handles the v2 blob registration path, including provider/model
extraction, unsafe flag enforcement, and nested option merging. It is used
by :class:`ActorRegistry` to keep the main registry module under the 500-line
limit.
"""
from __future__ import annotations
from typing import Any
import pydantic
from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.schema import ActorConfigSchema, is_v3_yaml
from cleveragents.application.services.actor_service import ActorService
from cleveragents.core.exceptions import NotFoundError, ValidationError
from cleveragents.domain.models.core import Actor
def add_legacy(
blob: dict[str, Any],
*,
yaml_text: str,
update: bool,
schema_version: str,
compiled_metadata: dict[str, Any] | None,
unsafe: bool = False,
allow_unsafe: bool = False,
actor_service: ActorService,
ensure_namespaced: callable, # type: ignore[type-arg]
) -> Actor:
"""Register an actor from a legacy (v2) blob.
Args:
blob: Parsed YAML blob.
yaml_text: Original YAML source text.
update: When ``True`` allow overwriting an existing actor.
schema_version: Schema version string.
compiled_metadata: Optional compiler-produced metadata dict.
unsafe: Asserts that the actor IS unsafe.
allow_unsafe: Permits registration of an already-unsafe actor.
actor_service: The actor persistence service.
ensure_namespaced: Function to namespace actor names.
Returns:
The persisted ``Actor`` domain object.
"""
name_raw: str = blob.get("name", "")
if not name_raw:
raise ValidationError("Actor YAML must include a 'name' field.")
# ── Validate v3 YAML via ActorConfigSchema if detected ──────────────────
# This ensures v3 actors are validated against the full schema, including
# cycle detection, required fields, and enum validation.
blob_is_v3 = is_v3_yaml(blob)
if blob_is_v3:
try:
ActorConfigSchema.model_validate(blob)
except pydantic.ValidationError as exc:
raise ValidationError(f"Invalid v3 actor configuration: {exc}") from exc
name = ensure_namespaced(name_raw)
provider_raw = blob.get("provider") or blob.get("provider_type", "")
model_raw = blob.get("model") or blob.get("model_id", "")
# ── Guard: reject multi-actor YAML ─────────────────────────────────
# ``add()`` is designed for single-actor YAML files. Provider, model,
# unsafe, and graph extraction all operate on the *first* entry only,
# so a multi-actor map would silently discard later entries (including
# their ``unsafe`` flags — a spec-compliance and security concern).
# Multi-actor configurations must be split and registered individually.
# Uses ``_resolve_actors_map()`` for consistent ``actors:``/``agents:``
# resolution across all call sites.
actors_map, _ = ActorConfiguration._resolve_actors_map(blob)
if isinstance(actors_map, dict) and len(actors_map) > 1:
raise ValidationError(
"registry.add() only supports single-actor YAML files. "
"Multi-actor configurations must be registered individually."
)
# Always extract from the nested ``actors:``/``agents:`` map so that
# ``unsafe`` and ``graph_descriptor`` are captured even when top-level
# ``provider``/``model`` are present. This matches the behaviour of
# ``from_blob()`` which unconditionally calls ``_extract_v2_actor()``.
nested_provider, nested_model, nested_graph, nested_unsafe = (
ActorConfiguration._extract_v2_actor(blob)
)
if not provider_raw and nested_provider:
provider_raw = nested_provider
if not model_raw and nested_model:
model_raw = nested_model
if not blob_is_v3 and (not provider_raw or not model_raw):
raise ValidationError("Actor YAML must include 'provider' and 'model' fields.")
# For v3 actors without provider/model (e.g. TOOL actors), provider_raw
# and model_raw may be empty strings here. This is expected — the legacy
# canonicalization path in upsert_actor() / from_blob() will reject
# provider-less TOOL actors. See follow-up issue #9971.
provider = str(provider_raw) if provider_raw else ""
model = str(model_raw) if model_raw else ""
top_unsafe_raw = blob.get("unsafe", False)
# Accept only boolean ``True`` or integer ``1`` as unsafe. Note:
# ``top_unsafe_raw == 1`` also matches ``1.0`` (float) due to
# Python's numeric equality rules — this is fail-safe (treats 1.0
# as unsafe).
#
# ``unsafe`` (the parameter) is an *assertion* — "this actor IS
# unsafe" (maps to the spec's "CLI --unsafe flag").
# ``allow_unsafe`` is a *permission* — "I PERMIT an already-unsafe
# actor to be registered" but does NOT itself make the actor unsafe.
# Therefore only ``unsafe`` participates in the stored value; the
# spec rule is: "the result is true if *any* of: top-level unsafe,
# nested unsafe, or the CLI --unsafe flag is true."
effective_unsafe = (
(top_unsafe_raw is True or top_unsafe_raw == 1) or nested_unsafe or unsafe
)
if effective_unsafe and not (unsafe or allow_unsafe):
raise ValidationError(
"Actor configuration is marked unsafe; pass unsafe=True "
"or allow_unsafe=True to confirm."
)
if not update:
try:
actor_service.get_actor(name)
except NotFoundError:
pass # Actor does not exist yet — proceed with add
else:
raise ValidationError(
f"Actor '{name}' already exists. Pass update=True to overwrite."
)
# ── Extract nested options so they are not silently discarded ─────
# ``from_blob()`` calls ``_extract_v2_options()`` internally, but
# ``add()`` bypasses ``from_blob()``. Without this call, options
# nested inside ``actors.<name>.config.options`` would be lost.
v2_options = ActorConfiguration._extract_v2_options(blob)
config_blob: dict[str, Any] = dict(blob)
config_blob.setdefault("source", "yaml")
if v2_options is not None:
existing_options = config_blob.get("options")
if existing_options is None or not isinstance(existing_options, dict):
# No valid top-level options dict — use nested options directly.
config_blob["options"] = v2_options
else:
# Both top-level and nested options exist. Match ``from_blob()``
# merge semantics: nested options as base, top-level overrides.
merged = dict(v2_options)
merged.update(existing_options)
config_blob["options"] = merged
top_graph_raw = blob.get("graph_descriptor")
top_graph = top_graph_raw if top_graph_raw is not None else blob.get("graph")
resolved_graph = top_graph if top_graph is not None else nested_graph
return actor_service.upsert_actor(
name=name,
provider=provider,
model=model,
config_blob=config_blob,
graph_descriptor=resolved_graph,
unsafe=effective_unsafe,
set_default=False,
yaml_text=yaml_text,
schema_version=schema_version,
compiled_metadata=compiled_metadata,
)
+5 -15
View File
@@ -26,7 +26,6 @@ import pydantic
import yaml
from cleveragents.actor.config import ActorConfiguration
from cleveragents.actor.legacy_registry import add_legacy
from cleveragents.actor.schema import ActorConfigSchema, is_v3_yaml
from cleveragents.actor.v3_registry import add_v3, is_v3_blob
from cleveragents.application.services.actor_service import ActorService
@@ -326,6 +325,7 @@ class ActorRegistry:
# v3 path: validate against ActorConfigSchema when detected
# ----------------------------------------------------------
if is_v3_blob(blob):
effective_allow_unsafe = allow_unsafe or unsafe
return add_v3(
blob,
yaml_text=yaml_text,
@@ -333,22 +333,12 @@ class ActorRegistry:
schema_version=version,
compiled_metadata=compiled_metadata,
actor_service=self._actor_service,
allow_unsafe=allow_unsafe,
allow_unsafe=effective_allow_unsafe,
)
# ----------------------------------------------------------
# Legacy (v2) path: flat provider/model extraction
# ----------------------------------------------------------
return add_legacy(
blob,
yaml_text=yaml_text,
update=update,
schema_version=version,
compiled_metadata=compiled_metadata,
unsafe=unsafe,
allow_unsafe=allow_unsafe,
actor_service=self._actor_service,
ensure_namespaced=self._ensure_namespaced,
raise ValidationError(
"Invalid actor configuration: no valid type field found. "
"Expected a v3 YAML with type: llm, type: graph, or type: tool."
)
# ------------------------------------------------------------------
+108 -2
View File
@@ -26,9 +26,111 @@ logger = logging.getLogger(__name__)
def is_v3_blob(blob: dict[str, Any]) -> bool:
"""Return ``True`` when *blob* looks like a v3 ``ActorConfigSchema``."""
"""Return ``True`` when *blob* looks like a v3 ``ActorConfigSchema``.
A blob is considered v3 if it has:
1. A top-level ``type`` field with a v3 value (llm/graph/tool), OR
2. An ``actors:`` map containing at least one entry whose ``type`` field
has a v3 value (nested actor format per spec line 21417).
The nested format allows configurations like:
name: local/my-actor
actors:
my_actor:
type: llm
config:
provider: anthropic
model: claude-3.5-sonnet
Returns:
True if the blob appears to be v3 format.
"""
actor_type = blob.get("type")
return isinstance(actor_type, str) and actor_type.lower() in _V3_ACTOR_TYPES
if isinstance(actor_type, str) and actor_type.lower() in _V3_ACTOR_TYPES:
return True
actors_val = blob.get("actors")
if isinstance(actors_val, dict) and actors_val:
for _, entry in actors_val.items():
if isinstance(entry, dict):
nested_type = entry.get("type")
if (
isinstance(nested_type, str)
and nested_type.lower() in _V3_ACTOR_TYPES
):
return True
return False
def _extract_nested_v3_config(data: dict[str, Any]) -> None:
"""Extract provider/model/type/description/name from nested actors.<name> block.
Per spec line 21417, v3 YAML may have provider/model at the top level
OR nested inside ``actors.<actor-name>.config``. When found in the nested
config, values are promoted to the top level.
Similarly, ``type``, ``description``, and ``name`` may be at the top level
OR nested inside ``actors.<actor-name>`` (for type/description) or
``actors.<actor-name>.config`` (for name). This function promotes all
of these from their nested locations when not present at the top level.
Args:
data: The actor blob (modified in place).
"""
actors_val = data.get("actors")
if not isinstance(actors_val, dict) or not actors_val:
return
for _, entry in actors_val.items():
if not isinstance(entry, dict):
continue
if "type" not in data:
type_val = entry.get("type")
if isinstance(type_val, str) and type_val:
data["type"] = type_val
if "description" not in data:
desc_val = entry.get("description")
if isinstance(desc_val, str) and desc_val:
data["description"] = desc_val
if "name" not in data:
name_val = entry.get("name")
if isinstance(name_val, str) and name_val:
data["name"] = name_val
config_block = entry.get("config")
if isinstance(config_block, dict):
if "model" not in data:
mv = config_block.get("model")
if isinstance(mv, str) and mv:
data["model"] = mv
if "provider" not in data:
pv = config_block.get("provider")
if isinstance(pv, str) and pv:
data["provider"] = pv
if "name" not in data:
name_val = config_block.get("name")
if isinstance(name_val, str) and name_val:
data["name"] = name_val
if "description" not in data:
desc_val = config_block.get("description")
if isinstance(desc_val, str) and desc_val:
data["description"] = desc_val
if (
"type" in data
and "description" in data
and "model" in data
and "provider" in data
):
break
def add_v3(
@@ -73,6 +175,10 @@ def add_v3(
if isinstance(name_raw, str) and name_raw and "/" not in name_raw:
data["name"] = f"local/{name_raw}"
# Extract provider and model from nested actors.<name>.config block if present.
# This handles the v3 nested format per spec line 21417.
_extract_nested_v3_config(data)
# Infer provider before schema validation — the schema now requires
# provider for LLM and GRAPH actors (added by #5869).
if not data.get("provider"):
@@ -13,6 +13,12 @@ from __future__ import annotations
from collections.abc import Iterator
from typing import Any
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_core.messages.tool import ToolMessage
from cleveragents.domain.models.core.session import MessageRole
from cleveragents.tool.actor_runtime import LLMResponse, LLMToolCall
# Token cost estimation constants (USD per 1K tokens).
# These are rough defaults; real cost comes from the LLM response metadata.
_COST_PER_1K_INPUT = 0.001
@@ -54,9 +60,7 @@ def extract_token_usage(response: Any) -> tuple[int, int]:
if usage:
try:
inp = int(usage.get("input_tokens") or usage.get("prompt_tokens") or 0)
out = int(
usage.get("output_tokens") or usage.get("completion_tokens") or 0
)
out = int(usage.get("output_tokens") or usage.get("completion_tokens") or 0)
return inp, out
except (TypeError, ValueError):
pass
@@ -94,15 +98,6 @@ def history_to_langchain_messages(
Returns an ordered list of LangChain message objects, empty if
no history is available.
"""
from langchain_core.messages import (
AIMessage,
HumanMessage,
SystemMessage,
)
from langchain_core.messages.tool import ToolMessage
from cleveragents.domain.models.core.session import MessageRole
lc_messages: list[Any] = []
for msg in messages:
role = msg.role
@@ -161,7 +156,6 @@ class LangChainSessionCaller:
self._last_response: Any = None
# Build initial accumulated messages: optional system + history
self._accumulated: list[Any] = []
from langchain_core.messages import SystemMessage
# Prepend system prompt only when history doesn't already have one
has_system = any(isinstance(m, SystemMessage) for m in history)
@@ -199,11 +193,6 @@ class LangChainSessionCaller:
:class:`~cleveragents.tool.actor_runtime.LLMResponse` with the
LLM's text response.
"""
from langchain_core.messages import HumanMessage
from langchain_core.messages.tool import ToolMessage
from cleveragents.tool.actor_runtime import LLMResponse, LLMToolCall
if self._first_call:
# First iteration: append the user's prompt and invoke
self._accumulated.append(HumanMessage(content=prompt))
@@ -21,6 +21,7 @@ from collections.abc import Callable, Iterator
from typing import TYPE_CHECKING, Any
import structlog
from langchain_core.messages import HumanMessage, SystemMessage
from pydantic import BaseModel, ConfigDict, Field
try:
@@ -406,8 +407,6 @@ class SessionWorkflow:
Used by ``tell_stream()`` to avoid the duplicate-user-message bug (C1).
The caller controls when history is sampled.
"""
from langchain_core.messages import HumanMessage, SystemMessage
lc_history = history_to_langchain_messages(history_messages)
messages: list[Any] = []
+138 -2
View File
@@ -27,7 +27,7 @@ import re
import shutil
import time
from contextlib import suppress
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path
from typing import TYPE_CHECKING, Annotated, Any, Literal, cast
@@ -4117,6 +4117,131 @@ def _get_decision_label(decision_type: str, per_type_ordinal: int = 0) -> str:
return base_label
def _build_tree_data(
plan_id: str,
tree_data: list[dict[str, object]],
decisions: list[Decision],
show_superseded: bool = False,
started_at: datetime | None = None,
) -> dict[str, object]:
"""Build the data payload for ``agents plan tree --format json/yaml``.
Returns the ``data`` dict that will be wrapped in the spec-required
command envelope by ``format_output``.
"""
filtered = (
decisions if show_superseded else [d for d in decisions if not d.is_superseded]
)
def count_nodes(nodes: list[dict[str, object]]) -> int:
count = 0
for node in nodes:
count += 1
children = node.get("children", [])
if isinstance(children, list):
count += count_nodes(children)
return count
def compute_depth(nodes: list[dict[str, object]]) -> int:
if not nodes:
return 0
max_depth = 0
for node in nodes:
children = node.get("children", [])
if isinstance(children, list) and children:
max_depth = max(max_depth, 1 + compute_depth(children))
return max_depth
nodes_count = count_nodes(tree_data)
tree_depth = compute_depth(tree_data)
child_plan_ids: set[str] = set()
for d in filtered:
if d.decision_type in ("subplan_spawn", "subplan_parallel_spawn") and d.plan_id:
child_plan_ids.add(d.plan_id)
child_plans_count = len(child_plan_ids)
child_plans_str = f"{child_plans_count}+" if child_plans_count > 0 else "0"
invariants_count = sum(
1 for d in filtered if d.decision_type == "invariant_enforced"
)
superseded_count = sum(1 for d in decisions if d.is_superseded)
summary = {
"nodes": nodes_count,
"depth": tree_depth,
"child_plans": child_plans_str,
"invariants": invariants_count,
"superseded": superseded_count,
}
type_counts: dict[str, int] = {}
decision_ids: dict[str, str] = {}
for d in filtered:
type_counts[d.decision_type] = type_counts.get(d.decision_type, 0) + 1
ordinal = type_counts[d.decision_type]
if d.decision_type == "prompt_definition":
key = "root"
elif d.decision_type == "invariant_enforced":
key = f"invariant_{ordinal}"
elif d.decision_type == "strategy_choice":
key = "strategy"
elif d.decision_type == "implementation_choice":
key = f"implementation_{ordinal}"
elif d.decision_type == "subplan_spawn":
key = f"spawn_{ordinal}"
elif d.decision_type == "subplan_parallel_spawn":
key = f"parallel_{ordinal}"
else:
key = f"{d.decision_type}_{ordinal}"
decision_ids[key] = d.decision_id
child_plans_list: list[dict[str, object]] = []
for d in filtered:
if d.decision_type in ("subplan_spawn", "subplan_parallel_spawn") and d.plan_id:
child_plans_list.append(
{
"id": d.plan_id,
"phase": "execute",
"state": "queued",
}
)
def convert_tree_node(node: dict[str, object]) -> dict[str, object]:
"""Convert internal tree node format to spec format."""
spec_node: dict[str, object] = {
"type": node.get("type"),
"description": node.get("question") or node.get("description"),
}
if node.get("confidence") is not None:
spec_node["confidence"] = node.get("confidence")
if node.get("type") in ("subplan_spawn", "subplan_parallel_spawn"):
spec_node["plan_id"] = node.get("plan_id", "")
children = node.get("children", [])
if isinstance(children, list) and children:
spec_node["children"] = [convert_tree_node(child) for child in children]
return spec_node
spec_tree = convert_tree_node(tree_data[0]) if tree_data else None
return {
"plan_id": plan_id,
"tree": spec_tree,
"summary": summary,
"child_plans": child_plans_list,
"decision_ids": decision_ids,
}
@app.command("tree")
def tree_decisions_cmd(
plan_id: Annotated[
@@ -4139,6 +4264,7 @@ def tree_decisions_cmd(
"""Display the decision tree for a plan."""
from cleveragents.application.container import get_container
_tree_cmd_start = datetime.now(UTC)
container = get_container()
svc = container.decision_service()
decisions = svc.list_decisions(plan_id)
@@ -4153,7 +4279,17 @@ def tree_decisions_cmd(
)
if fmt in (OutputFormat.JSON, OutputFormat.YAML):
console.print(format_output(tree_data, fmt))
tree_data_dict = _build_tree_data(
plan_id, tree_data, decisions, show_superseded, started_at=_tree_cmd_start
)
console.print(
format_output(
tree_data_dict,
fmt,
command="plan tree",
messages=[{"level": "ok", "text": "Decision tree rendered"}],
)
)
elif fmt == OutputFormat.TABLE:
# Flatten for table view
filtered = (
Generated
+2
View File
@@ -446,6 +446,7 @@ dependencies = [
{ name = "pydantic" },
{ name = "pydantic-settings" },
{ name = "python-ulid" },
{ name = "pyyaml" },
{ name = "restrictedpython" },
{ name = "rx" },
{ name = "structlog" },
@@ -528,6 +529,7 @@ requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
{ name = "pyyaml", specifier = ">=6.0.3" },
{ name = "python-ulid", specifier = ">=2.7.0" },
{ name = "radon", marker = "extra == 'dev'", specifier = ">=6.0.1" },
{ name = "restrictedpython", specifier = ">=7.0" },