{"record":{"id":"2e0326b1669b2a7b","repo":"ruvnet/ruflo","slug":"not-claimed","errorCode":"NOT_CLAIMED","errorMessage":"Issue ${issueId} is not claimed","messagePattern":"Issue (.+?) is not claimed","errorType":"exception","errorClass":"ClaimOperationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/claims/src/application/claim-service.ts","lineNumber":247,"sourceCode":"      reviewers: [],\n    };\n\n    // Save claim\n    await this.claimRepository.save(claim);\n\n    // Emit event\n    const event = createClaimCreatedEvent(claimId, issueId, claimant);\n    await this.eventStore.append(event);\n\n    return { success: true, claim };\n  }\n\n  async release(issueId: string, claimant: Claimant): 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 claimant owns the claim\n    if (claim.claimant.id !== claimant.id) {\n      throw new ClaimOperationError(\n        'UNAUTHORIZED',\n        `Claimant ${claimant.name} does not own the claim on issue ${issueId}`\n      );\n    }\n\n    // Check for pending handoffs\n    const pendingHandoff = claim.handoffChain?.find((h) => h.status === 'pending');\n    if (pendingHandoff) {\n      throw new ClaimOperationError(\n        'HANDOFF_PENDING',\n        `Cannot release claim with pending handoff to ${pendingHandoff.to.name}`\n      );\n    }","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/claims/src/application/claim-service.ts#L229-L265","documentation":"ClaimOperationError with code NOT_CLAIMED, thrown by ClaimService.release in @claude-flow/claims when claimRepository.findByIssueId(issueId) returns null. It means there is no claim record at all for the issue in the backing repository, so releasing is an invalid state transition — distinct from an ownership failure (UNAUTHORIZED) which is checked next.","triggerScenarios":"Calling release() on an issue whose claim was already released or expired and evicted, or never claimed; repository persistence lost/reset between claim and release; wrong issueId passed to release().","commonSituations":"Idempotent cleanup routines calling release on every issue in a shutdown sweep, including unclaimed ones; duplicate release calls after a timeout-retry; multi-instance setups where another node already released and the repository state propagated.","solutions":["Check for an existing claim (findByIssueId) before releasing and treat null as already-released/no-op","Catch ClaimOperationError and switch on code === 'NOT_CLAIMED' to skip gracefully in cleanup paths","Fix retry logic to re-read claim state instead of blindly repeating release"],"exampleFix":"// before\nawait claimService.release(issueId, claimant);\n\n// after\nconst existing = await claimRepository.findByIssueId(issueId);\nif (!existing) return; // nothing to release — idempotent cleanup\nawait claimService.release(issueId, claimant);","handlingStrategy":"try-catch","validationCode":"const claim = await claimRepository.findByIssueId(issueId);\nif (!claim) return; // already unclaimed — release is a no-op\nawait claimService.release(issueId, claimant);","typeGuard":null,"tryCatchPattern":"try { await claimService.release(issueId, claimant); } catch (e) { if (e instanceof ClaimOperationError && e.code === 'NOT_CLAIMED') return; /* idempotent */ throw e; }","preventionTips":["Pre-check findByIssueId in cleanup paths","Catch ClaimOperationError and branch on .code rather than message text"],"tags":["claims","claim-lifecycle","release","invalid-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}