4.3 KiB
Invariants
Invariants are natural-language constraints on plan execution. They flow into the decision tree during Strategize and are reconciled by the Invariant Reconciliation Actor before any strategy work begins.
Scope Hierarchy
| Scope | Description |
|---|---|
GLOBAL |
Applies to every plan in the system |
PROJECT |
Applies to plans targeting a specific project |
ACTION |
Defined in an action template; promoted on plan use |
PLAN |
Attached directly to a specific plan |
Merge Precedence
When computing the effective set of invariants for a plan, the precedence chain is:
plan > project > global
- Plan-level invariants take highest precedence.
- Project-level invariants apply next.
- Global-level invariants are lowest precedence.
- Action-level invariants are promoted to plan scope when
plan useis called.
De-duplication
Invariants are de-duplicated by text (case-insensitive). When the same constraint text appears at multiple scopes, only the highest-precedence copy is kept.
Merge Example
Given:
- Global:
"Never delete production data","Log all changes" - Project (
myapp):"All API changes need tests","Log all changes" - Plan:
"Use Python 3.13 only"
The effective set for a plan targeting myapp would be:
"Use Python 3.13 only"(plan)"All API changes need tests"(project)"Log all changes"(project — shadows the global duplicate)"Never delete production data"(global)
Enforcement
At the start of the Strategize phase, the Invariant Reconciliation Actor:
- Collects the effective invariant set for the plan.
- Evaluates each invariant against the current plan context.
- Creates an
InvariantEnforcementRecordfor each invariant. - Records an
invariant_enforceddecision in the decision tree.
Enforcement Record
Each record contains:
| Field | Description |
|---|---|
invariant_id |
ULID of the invariant |
enforced |
Whether the invariant was successfully enforced |
actor_response |
Response text from the reconciliation actor |
decision_id |
ULID of the associated decision node |
Violation Model
When an invariant is violated during execution, an InvariantViolation is created:
| Field | Description |
|---|---|
invariant_id |
ULID of the violated invariant |
violated_text |
The invariant text that was violated |
severity |
error, warning, or info |
details |
Additional violation context |
CLI Usage
Add an invariant
# Global invariant
agents invariant add --global "Never delete production data"
# Project-scoped invariant
agents invariant add --project myapp "All API changes need tests"
# Plan-specific invariant
agents invariant add --plan 01HXYZ... "Use Python 3.13 only"
# Action-scoped invariant
agents invariant add --action local/code-coverage "Minimum 80% coverage"
List invariants
# List all active invariants
agents invariant list
# Filter by scope
agents invariant list --global
agents invariant list --project myapp
# Show merged effective set for a project
agents invariant list --effective --project myapp
# Filter by regex
agents invariant list "data.*safe"
# JSON output
agents invariant list --format json
Remove an invariant
# With confirmation prompt
agents invariant remove 01HXYZ...
# Skip confirmation
agents invariant remove --yes 01HXYZ...
Relationship with PlanInvariant
The existing PlanInvariant model in plan.py represents invariants already
attached to a plan with an InvariantSource provenance tag. The new Invariant
model in invariant.py is the standalone storage model used by InvariantService
for cross-scope management. When a plan is created via plan use, action
invariants are converted to PlanInvariant entries on the plan itself.