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

  1. Call getValidStatusTransitions(claim.status) (or read claim.status then the table) and only invoke updateStatus if the target is included.
  2. For completed work that must be reopened, release the old claim and create a new one rather than transitioning.
  3. Verify the status argument is a literal of ClaimStatus and uses the exact form the service expects.
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/30b5e7b8c75c519c. Report an issue: GitHub.