UAT: merge_invariants and InvariantSet.merge missing action scope — four-tier precedence chain not implemented #5056

Closed
opened 2026-04-09 00:52:04 +00:00 by HAL9000 · 1 comment
Owner

Bug Report

Feature Area: ACMS and Context Management — Invariants

Severity: Critical (blocks correct invariant precedence resolution)


What Was Tested

Code-level analysis of the invariant merge logic in:

  • src/cleveragents/domain/models/core/invariant.py
  • src/cleveragents/application/services/invariant_service.py

Expected Behavior (from spec)

The specification (§Glossary — Invariant, line 92) defines a four-tier runtime precedence chain:

The runtime precedence chain is four-tier: plan > action > project > global. Exception: global invariants marked non_overridable always win regardless of scope.

The InvariantReconciliationActor in src/cleveragents/actor/reconciliation.py correctly implements this four-tier chain with _SCOPE_PRECEDENCE:

_SCOPE_PRECEDENCE: dict[InvariantScope, int] = {
    InvariantScope.PLAN: 0,
    InvariantScope.ACTION: 1,
    InvariantScope.PROJECT: 2,
    InvariantScope.GLOBAL: 3,
}

Actual Behavior (bug)

The merge_invariants() function and InvariantSet.merge() class method in invariant.py only implement a three-tier chain, completely omitting the action scope:

def merge_invariants(
    plan_invariants: list[Invariant],
    project_invariants: list[Invariant],
    global_invariants: list[Invariant],   # ← no action_invariants parameter!
) -> list[Invariant]:
    """Merge invariants implementing plan > project > global precedence."""
    ...
    for inv_list in (plan_invariants, project_invariants, global_invariants):
        ...

Similarly, InvariantSet.merge() only accepts three parameters:

@classmethod
def merge(
    cls,
    plan_invariants: list[Invariant],
    project_invariants: list[Invariant],
    global_invariants: list[Invariant],   # ← no action_invariants!
) -> InvariantSet:
    """Merge invariants respecting plan > project > global precedence."""

And InvariantService.get_effective_invariants() also only handles three tiers:

def get_effective_invariants(
    self,
    plan_id: str | None = None,
    project_name: str | None = None,   # ← no action_name parameter!
) -> list[Invariant]:
    ...
    return merge_invariants(plan_invs, project_invs, global_invs)
    # action_invs are never collected or merged!

Impact

When InvariantService.get_effective_invariants() is called (e.g., by agents invariant list --effective), action-scoped invariants are silently dropped from the effective set. This means:

  1. Action-level invariants defined in action YAML templates are not included in the effective invariant view when using the service-layer merge path.
  2. The InvariantSet.merge() API cannot represent the full four-tier chain.
  3. Any code that calls merge_invariants() directly (rather than going through InvariantReconciliationActor) will produce incorrect results.

Note: InvariantReconciliationActor.run() is correct and does handle all four scopes. The bug is in the lower-level utility functions that bypass the actor.


Steps to Reproduce

  1. Add a global invariant: agents invariant add --global "Never delete production data"
  2. Add an action invariant: agents invariant add --action my-action "Action-level constraint"
  3. Add a project invariant: agents invariant add --project local/myapp "Project constraint"
  4. Call InvariantService.get_effective_invariants(project_name="local/myapp") — the action invariant is absent from the result.

Code Locations

  • src/cleveragents/domain/models/core/invariant.pymerge_invariants() (line 166), InvariantSet.merge() (line 136)
  • src/cleveragents/application/services/invariant_service.pyget_effective_invariants() (line 167)

Fix Required

  1. Add action_invariants: list[Invariant] parameter to merge_invariants() and InvariantSet.merge().
  2. Update InvariantService.get_effective_invariants() to accept action_name: str | None = None and collect action-scoped invariants.
  3. Update the merge loop to iterate (plan_invariants, action_invariants, project_invariants, global_invariants) in that order.

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: uat-tester

## Bug Report **Feature Area:** ACMS and Context Management — Invariants **Severity:** Critical (blocks correct invariant precedence resolution) --- ## What Was Tested Code-level analysis of the invariant merge logic in: - `src/cleveragents/domain/models/core/invariant.py` - `src/cleveragents/application/services/invariant_service.py` --- ## Expected Behavior (from spec) The specification (§Glossary — Invariant, line 92) defines a **four-tier** runtime precedence chain: > The runtime precedence chain is four-tier: **plan > action > project > global**. Exception: global invariants marked `non_overridable` always win regardless of scope. The `InvariantReconciliationActor` in `src/cleveragents/actor/reconciliation.py` correctly implements this four-tier chain with `_SCOPE_PRECEDENCE`: ```python _SCOPE_PRECEDENCE: dict[InvariantScope, int] = { InvariantScope.PLAN: 0, InvariantScope.ACTION: 1, InvariantScope.PROJECT: 2, InvariantScope.GLOBAL: 3, } ``` --- ## Actual Behavior (bug) The `merge_invariants()` function and `InvariantSet.merge()` class method in `invariant.py` only implement a **three-tier** chain, completely omitting the `action` scope: ```python def merge_invariants( plan_invariants: list[Invariant], project_invariants: list[Invariant], global_invariants: list[Invariant], # ← no action_invariants parameter! ) -> list[Invariant]: """Merge invariants implementing plan > project > global precedence.""" ... for inv_list in (plan_invariants, project_invariants, global_invariants): ... ``` Similarly, `InvariantSet.merge()` only accepts three parameters: ```python @classmethod def merge( cls, plan_invariants: list[Invariant], project_invariants: list[Invariant], global_invariants: list[Invariant], # ← no action_invariants! ) -> InvariantSet: """Merge invariants respecting plan > project > global precedence.""" ``` And `InvariantService.get_effective_invariants()` also only handles three tiers: ```python def get_effective_invariants( self, plan_id: str | None = None, project_name: str | None = None, # ← no action_name parameter! ) -> list[Invariant]: ... return merge_invariants(plan_invs, project_invs, global_invs) # action_invs are never collected or merged! ``` --- ## Impact When `InvariantService.get_effective_invariants()` is called (e.g., by `agents invariant list --effective`), action-scoped invariants are silently dropped from the effective set. This means: 1. Action-level invariants defined in action YAML templates are not included in the effective invariant view when using the service-layer merge path. 2. The `InvariantSet.merge()` API cannot represent the full four-tier chain. 3. Any code that calls `merge_invariants()` directly (rather than going through `InvariantReconciliationActor`) will produce incorrect results. Note: `InvariantReconciliationActor.run()` is correct and does handle all four scopes. The bug is in the lower-level utility functions that bypass the actor. --- ## Steps to Reproduce 1. Add a global invariant: `agents invariant add --global "Never delete production data"` 2. Add an action invariant: `agents invariant add --action my-action "Action-level constraint"` 3. Add a project invariant: `agents invariant add --project local/myapp "Project constraint"` 4. Call `InvariantService.get_effective_invariants(project_name="local/myapp")` — the action invariant is absent from the result. --- ## Code Locations - `src/cleveragents/domain/models/core/invariant.py` — `merge_invariants()` (line 166), `InvariantSet.merge()` (line 136) - `src/cleveragents/application/services/invariant_service.py` — `get_effective_invariants()` (line 167) --- ## Fix Required 1. Add `action_invariants: list[Invariant]` parameter to `merge_invariants()` and `InvariantSet.merge()`. 2. Update `InvariantService.get_effective_invariants()` to accept `action_name: str | None = None` and collect action-scoped invariants. 3. Update the merge loop to iterate `(plan_invariants, action_invariants, project_invariants, global_invariants)` in that order. --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: uat-tester
Author
Owner

Closing as duplicate of #4825 — both issues report the same problem: merge_invariants and InvariantSet.merge missing ACTION tier in the four-tier precedence chain.


Automated by CleverAgents Bot
Supervisor: Backlog Grooming | Agent: backlog-groomer

Closing as duplicate of #4825 — both issues report the same problem: `merge_invariants` and `InvariantSet.merge` missing ACTION tier in the four-tier precedence chain. --- **Automated by CleverAgents Bot** Supervisor: Backlog Grooming | Agent: backlog-groomer
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#5056
No description provided.