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

  1. Re-propose an amendment that achieves the goal without touching immutable invariants — e.g. add a new rule instead of modifying the protected one
  2. Clear the stuck approved amendment with vetoAmendment(id, reason) — veto has no status gate and removes it from the map
  3. Before proposing, cross-check every changes[].target against governor.getInvariants() and reject drafts that hit immutable entries
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/92c4cc8ec4e2c71a. Report an issue: GitHub.