{"record":{"id":"30b5e7b8c75c519c","repo":"ruvnet/ruflo","slug":"invalid-status-transition","errorCode":"INVALID_STATUS_TRANSITION","errorMessage":"Cannot transition from ${claim.status} to ${status}","messagePattern":"Cannot transition from (.+?) to (.+?)","errorType":"error_code","errorClass":"ClaimOperationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/claims/src/application/claim-service.ts","lineNumber":483,"sourceCode":"    await this.eventStore.append(statusEvent);\n  }\n\n  // ==========================================================================\n  // Status\n  // ==========================================================================\n\n  async updateStatus(issueId: string, status: ClaimStatus, note?: string): Promise<void> {\n    const claim = await this.claimRepository.findByIssueId(issueId);\n\n    // Validate claim exists\n    if (!claim) {\n      throw new ClaimOperationError('NOT_CLAIMED', `Issue ${issueId} is not claimed`);\n    }\n\n    // Validate status transition\n    const validTransitions = this.getValidStatusTransitions(claim.status);\n    if (!validTransitions.includes(status)) {\n      throw new ClaimOperationError(\n        'INVALID_STATUS_TRANSITION',\n        `Cannot transition from ${claim.status} to ${status}`,\n        { currentStatus: claim.status, requestedStatus: status, validTransitions }\n      );\n    }\n\n    const previousStatus = claim.status;\n    claim.status = status;\n    claim.lastActivityAt = new Date();\n\n    // Add note if provided\n    if (note) {\n      claim.notes = claim.notes ?? [];\n      claim.notes.push(`[${new Date().toISOString()}] Status changed to ${status}: ${note}`);\n    }\n\n    await this.claimRepository.save(claim);\n","sourceCodeStart":465,"sourceCodeEnd":501,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/claims/src/application/claim-service.ts#L465-L501","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait claimService.updateStatus(issueId, 'active', 'reopen'); // was 'completed' -> throws\n\n// after\nconst { claim } = await claimService.getIssueStatus(issueId);\nconst allowed = getValidStatusTransitions(claim!.status);\nif (allowed.includes('active')) {\n  await claimService.updateStatus(issueId, 'active', 'reopen');\n} else {\n  await claimService.release(issueId, claim!.claimant);\n  await claimService.claim(issueId, claim!.claimant);\n}","handlingStrategy":"type-guard","validationCode":"import { getValidStatusTransitions } from '../domain/types.js';\nconst { claim } = await claimService.getIssueStatus(issueId);\nif (!claim) throw new Error('no claim');\nif (!getValidStatusTransitions(claim.status).includes(nextStatus)) {\n  throw new Error(`${claim.status} -> ${nextStatus} not allowed`);\n}\nawait claimService.updateStatus(issueId, nextStatus, note);","typeGuard":"function isValidTransition(from: ClaimStatus, to: ClaimStatus): boolean {\n  return getValidStatusTransitions(from).includes(to);\n}","tryCatchPattern":"try {\n  await claimService.updateStatus(issueId, nextStatus, note);\n} catch (e) {\n  if (e instanceof ClaimOperationError && e.code === 'INVALID_STATUS_TRANSITION') {\n    // details.validTransitions lists allowed targets; reconcile or reopen via new claim\n    const allowed = (e.details?.validTransitions as ClaimStatus[]) ?? [];\n    throw new Error(`Invalid transition; allowed: ${allowed.join(', ')}`);\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["claims","status","state-machine","validation"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}