ruvnet/ruflo · error · ClaimOperationError
INVALID_STATUS_TRANSITION
INVALID_STATUS_TRANSITION
Error message
Cannot transition from ${claim.status} to ${status} What it means
Thrown by updateStatus when the requested status is not in getValidStatusTransitions(claim.status). The claim lifecycle is a state machine: notably 'completed' is terminal (no outbound transitions), and 'released'/'expired' are not reachable transition targets through this method. The error carries details { currentStatus, requestedStatus, validTransitions } so the caller can see the allowed set.
Source
Thrown at v3/@claude-flow/claims/src/application/claim-service.ts:483
await this.eventStore.append(statusEvent);
}
// ==========================================================================
// Status
// ==========================================================================
async updateStatus(issueId: string, status: ClaimStatus, note?: string): Promise<void> {
const claim = await this.claimRepository.findByIssueId(issueId);
// Validate claim exists
if (!claim) {
throw new ClaimOperationError('NOT_CLAIMED', `Issue ${issueId} is not claimed`);
}
// Validate status transition
const validTransitions = this.getValidStatusTransitions(claim.status);
if (!validTransitions.includes(status)) {
throw new ClaimOperationError(
'INVALID_STATUS_TRANSITION',
`Cannot transition from ${claim.status} to ${status}`,
{ currentStatus: claim.status, requestedStatus: status, validTransitions }
);
}
const previousStatus = claim.status;
claim.status = status;
claim.lastActivityAt = new Date();
// Add note if provided
if (note) {
claim.notes = claim.notes ?? [];
claim.notes.push(`[${new Date().toISOString()}] Status changed to ${status}: ${note}`);
}
await this.claimRepository.save(claim);
View on GitHub (pinned to 6b01dc5a68)
Solutions
- Call getValidStatusTransitions(claim.status) (or read claim.status then the table) and only invoke updateStatus if the target is included.
- For completed work that must be reopened, release the old claim and create a new one rather than transitioning.
- Verify the status argument is a literal of ClaimStatus and uses the exact form the service expects.
- Inspect the thrown error's details.validTransitions to reconcile your expectation with the service's table.
Example fix
// before
await claimService.updateStatus(issueId, 'active', 'reopen'); // was 'completed' -> throws
// after
const { claim } = await claimService.getIssueStatus(issueId);
const allowed = getValidStatusTransitions(claim!.status);
if (allowed.includes('active')) {
await claimService.updateStatus(issueId, 'active', 'reopen');
} else {
await claimService.release(issueId, claim!.claimant);
await claimService.claim(issueId, claim!.claimant);
} Defensive patterns
Strategy: type-guard
Validate before calling
import { getValidStatusTransitions } from '../domain/types.js';
const { claim } = await claimService.getIssueStatus(issueId);
if (!claim) throw new Error('no claim');
if (!getValidStatusTransitions(claim.status).includes(nextStatus)) {
throw new Error(`${claim.status} -> ${nextStatus} not allowed`);
}
await claimService.updateStatus(issueId, nextStatus, note); Type guard
function isValidTransition(from: ClaimStatus, to: ClaimStatus): boolean {
return getValidStatusTransitions(from).includes(to);
} Try / catch
try {
await claimService.updateStatus(issueId, nextStatus, note);
} catch (e) {
if (e instanceof ClaimOperationError && e.code === 'INVALID_STATUS_TRANSITION') {
// details.validTransitions lists allowed targets; reconcile or reopen via new claim
const allowed = (e.details?.validTransitions as ClaimStatus[]) ?? [];
throw new Error(`Invalid transition; allowed: ${allowed.join(', ')}`);
}
throw e;
} Prevention
- Never assume 'completed' can revert; release + re-claim to reopen.
- Drive the UI's enabled actions from getValidStatusTransitions(currentStatus).
- Treat ClaimStatus as opaque literals; avoid constructing them from free text.
When it happens
Trigger: Reopening a completed claim (completed -> active is invalid); transitioning from a terminal or released/expired state; moving handoff-pending to blocked or paused (not in the table); passing a status string that is not a valid ClaimStatus literal.
Common situations: An 'Undo/Reopen' button on completed work; workflows that assume any status can follow any other; status string typos or casing errors; mixing underscored statuses ('in_review') with hyphenated variants.
Related errors
- Invalid target format: ${target}. Use agent:<id> or human:<i
- Issue ${input.issueId} is not stealable or not claimed
- HANDOFF_PENDING
- No contest to resolve
- unknown game "${key}". Known: ${Object.keys(GAMES).join(', '
AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12).
Data as JSON: /api/errors/30b5e7b8c75c519c.
Report an issue: GitHub.