ruvnet/ruflo · error
Cannot modify immutable invariant: ${change.target}
Error message
Cannot modify immutable invariant: ${change.target} What it means
During enactAmendment(), every change with type 'remove-rule' or 'modify-rule' is checked against the invariants map; if change.target names an invariant flagged immutable: true, enactment throws even though the amendment was approved. The check runs before any mutation, so the amendment remains in the map with status 'approved' (and can still be vetoed). This is the constitution's hard floor: even supermajority approval cannot touch immutable invariants.
Source
Thrown at v3/@claude-flow/guidance/src/meta-governance.ts:443
/**
* Enact an approved amendment
* Returns true if enacted successfully
*/
enactAmendment(amendmentId: string): boolean {
const amendment = this.amendments.get(amendmentId);
if (!amendment) {
throw new Error(`Amendment not found: ${amendmentId}`);
}
if (amendment.status !== 'approved') {
throw new Error(`Cannot enact amendment with status: ${amendment.status}`);
}
// Check if any changes would violate immutable invariants
for (const change of amendment.changes) {
if (change.type === 'remove-rule' || change.type === 'modify-rule') {
const invariant = this.invariants.get(change.target);
if (invariant?.immutable) {
throw new Error(`Cannot modify immutable invariant: ${change.target}`);
}
}
}
amendment.status = 'enacted';
this.amendmentHistory.push(amendment);
this.amendments.delete(amendmentId);
return true;
}
/**
* Emergency veto of an amendment
*/
vetoAmendment(amendmentId: string, reason: string): void {
const amendment = this.amendments.get(amendmentId);
if (!amendment) {
throw new Error(`Amendment not found: ${amendmentId}`);View on GitHub (pinned to fa13ee4ad6)
Solutions
- Re-propose an amendment that achieves the goal without touching immutable invariants — e.g. add a new rule instead of modifying the protected one
- Clear the stuck approved amendment with vetoAmendment(id, reason) — veto has no status gate and removes it from the map
- Before proposing, cross-check every changes[].target against governor.getInvariants() and reject drafts that hit immutable entries
- If the invariant legitimately must change, rebuild the MetaGovernor at construction time with the new invariant definition — the runtime API will never allow it
Example fix
// before
const a = governor.proposeAmendment({ changes: [{ type: 'modify-rule', target: 'core-safety' }], /* ... */ });
// ... approved, then:
governor.enactAmendment(a.id); // throws: immutable invariant
// after — pre-validate targets before proposing
const immutable = new Set(
governor.getInvariants().filter(i => i.immutable).map(i => i.id)
);
const safe = changes.filter(c => !immutable.has(c.target));
if (safe.length !== changes.length) {
throw new Error('Amendment targets immutable invariants; rewrite the changes');
}
governor.proposeAmendment({ changes: safe, /* ... */ }); Defensive patterns
Strategy: validation
Validate before calling
const immutableIds = new Set(
governor.getInvariants().filter(i => i.immutable).map(i => i.id)
);
const touchesImmutable = amendment.changes.some(
c => (c.type === 'remove-rule' || c.type === 'modify-rule') && immutableIds.has(c.target)
);
if (touchesImmutable) {
throw new Error('Amendment targets immutable invariants; rewrite its changes before enacting');
}
governor.enactAmendment(amendment.id); Type guard
function amendmentIsEnactable(governor: MetaGovernor, amendment: Amendment): boolean {
const immutable = new Set(governor.getInvariants().filter(i => i.immutable).map(i => i.id));
return amendment.changes.every(
c => !((c.type === 'remove-rule' || c.type === 'modify-rule') && immutable.has(c.target))
);
} Prevention
- Validate every changes[].target against getInvariants() at proposal-draft time, not at enact time
- Prefer add-rule changes over modify-rule when the target is constitutional
- If enact throws this, the amendment remains 'approved' in the map — veto it and re-propose a safe version
When it happens
Trigger: An approved amendment whose changes[] include a remove-rule/modify-rule entry targeting an invariant registered with immutable: true; replaying a governance log against a governor whose core invariants are immutable; amendment drafts written without consulting getInvariants().
Common situations: Teams modelling policy changes as amendments and forgetting which invariants are protected; environments where the invariant set differs between staging (mutable) and production (immutable).
Related errors
- Cannot remove immutable invariant: ${id}
- Amendment rate limit exceeded: ${this.maxAmendmentsPerWindow
- Amendment not found: ${amendmentId}
- Cannot vote on amendment with status: ${amendment.status}
- Amendment already resolved: ${amendment.status}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/92c4cc8ec4e2c71a.
Report an issue: GitHub.