ruvnet/ruflo · error
Amendment already resolved: ${amendment.status}
Error message
Amendment already resolved: ${amendment.status} What it means
resolveAmendment() only operates on amendments whose status is still 'proposed'. Resolution mutates status in place to 'approved' or 'rejected' (based on supermajority threshold and requiredApprovals) without removing the entry, so a second resolve call finds a non-proposed status and throws 'Amendment already resolved' with the terminal status in the message.
Source
Thrown at v3/@claude-flow/guidance/src/meta-governance.ts:409
throw new Error(`Amendment not found: ${amendmentId}`);
}
if (amendment.status !== 'proposed') {
throw new Error(`Cannot vote on amendment with status: ${amendment.status}`);
}
amendment.votes.set(voterId, approve);
}
/**
* Resolve an amendment (check if supermajority reached)
*/
resolveAmendment(amendmentId: string): Amendment {
const amendment = this.amendments.get(amendmentId);
if (!amendment) {
throw new Error(`Amendment not found: ${amendmentId}`);
}
if (amendment.status !== 'proposed') {
throw new Error(`Amendment already resolved: ${amendment.status}`);
}
const totalVotes = amendment.votes.size;
const approvals = Array.from(amendment.votes.values()).filter((v) => v).length;
const approvalRate = totalVotes > 0 ? approvals / totalVotes : 0;
if (approvalRate >= this.supermajorityThreshold && approvals >= amendment.requiredApprovals) {
amendment.status = 'approved';
} else {
amendment.status = 'rejected';
}
return amendment;
}
/**
* Enact an approved amendment
* Returns true if enacted successfullyView on GitHub (pinned to fa13ee4ad6)
Solutions
- Treat the Amendment return value of the first resolveAmendment() as the verdict and persist it — do not re-resolve
- Make retries idempotent: check getPendingAmendments() (which only lists status 'proposed') before resolving
- Catch this specific message and return the previously recorded outcome instead of failing the retry
- In multi-worker setups, route resolution through a single owner
Example fix
// before
function settle(id: string) {
return governor.resolveAmendment(id); // throws on retry
}
// after
const verdicts = new Map<string, Amendment>();
function settle(id: string): Amendment {
if (verdicts.has(id)) return verdicts.get(id)!;
const pending = governor.getPendingAmendments().some(a => a.id === id);
const verdict = pending ? governor.resolveAmendment(id) : null;
if (verdict) verdicts.set(id, verdict);
return verdict!;
} Defensive patterns
Strategy: validation
Validate before calling
const stillProposed = governor.getPendingAmendments().some(a => a.id === amendmentId);
if (!stillProposed) {
// already approved/rejected (still in map but not pending), or terminal
const hist = governor.getAmendmentHistory().find(a => a.id === amendmentId);
return hist ?? null;
}
return governor.resolveAmendment(amendmentId); Type guard
function needsResolution(governor: MetaGovernor, id: string): boolean {
return governor.getPendingAmendments().some(a => a.id === id); // only 'proposed' need resolving
} Try / catch
try {
governor.resolveAmendment(amendmentId);
} catch (err) {
if (err instanceof Error && err.message.startsWith('Amendment already resolved')) {
return; // verdict already recorded; do not fail the retry job
}
throw err;
} Prevention
- Persist the resolveAmendment() return value; it is the authoritative verdict
- Give resolution a single owner in multi-worker setups to avoid double-resolve
- Build retry jobs that check getPendingAmendments() before re-invoking
When it happens
Trigger: Calling resolveAmendment() twice on the same ID; retry logic that re-runs resolve after a timeout even though the first call succeeded; a coordinator and a cleanup job both trying to resolve.
Common situations: At-least-once task redelivery without idempotency keys; polling loops that resolve on a timer; test helpers that resolve then the test body resolves again.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Cannot vote on amendment with status: ${amendment.status}
- Cannot enact amendment with status: ${amendment.status}
- Amendment rate limit exceeded: ${this.maxAmendmentsPerWindow
- Amendment not found: ${amendmentId}
- Cannot modify immutable invariant: ${change.target}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/81a82a9f2c3d27d3.
Report an issue: GitHub.