{"record":{"id":"10229aad006d3add","repo":"ruvnet/ruflo","slug":"handoff-pending","errorCode":"HANDOFF_PENDING","errorMessage":"Cannot release claim with pending handoff to ${pendingHandoff.to.name}","messagePattern":"Cannot release claim with pending handoff to (.+?)","errorType":"error_code","errorClass":"ClaimOperationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/claims/src/application/claim-service.ts","lineNumber":261,"sourceCode":"    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    }\n\n    // Update claim status\n    const previousStatus = claim.status;\n    claim.status = 'released';\n    claim.lastActivityAt = new Date();\n\n    await this.claimRepository.save(claim);\n\n    // Emit events\n    const releaseEvent = createClaimReleasedEvent(claim.id, issueId, claimant);\n    await this.eventStore.append(releaseEvent);\n\n    if (previousStatus !== 'released') {\n      const statusEvent = createClaimStatusChangedEvent(","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/claims/src/application/claim-service.ts#L243-L279","documentation":"Thrown by ClaimService.release when claim.handoffChain contains a handoff with status 'pending'. A claim mid-handoff cannot be released — doing so would orphan the pending transfer. Code is HANDOFF_PENDING, and the message names the pending handoff's target.","triggerScenarios":"Calling release after requestHandoff but before the target accepts/rejects; a handoff that was requested and never resolved.","commonSituations":"A handoff abandoned by the target; the caller forgetting a handoff is in flight; orchestrator releasing a worker's claim while a handoff to another worker is pending.","solutions":["Resolve the pending handoff first (accept or reject via the handoff APIs) before releasing.","If the handoff is stale, add a handoff cancellation/expire step (or extend the service) then release.","Check claim.handoffChain for any pending entry before calling release."],"exampleFix":"// before\nawait service.release(issueId, claimant); // throws HANDOFF_PENDING\n\n// after\nconst pending = claim.handoffChain?.find(h => h.status === 'pending');\nif (pending) {\n  await service.respondHandoff(issueId, pending.id, 'rejected', /*by*/ pending.to);\n}\nawait service.release(issueId, claimant);","handlingStrategy":"validation","validationCode":"function hasPendingHandoff(claim) {\n  return !!claim?.handoffChain?.some(h => h.status === 'pending');\n}","typeGuard":"function hasPendingHandoff(claim) { return !!claim?.handoffChain?.some(h => h.status === 'pending'); }","tryCatchPattern":"try { await service.release(issueId, claimant); } catch (e) {\n  if (e instanceof ClaimOperationError && e.code === 'HANDOFF_PENDING') { /* resolve handoff first */ } else throw e;\n}","preventionTips":["Resolve pending handoffs (accept/reject) before release.","Add a handoff expire/cancel path for abandoned handoffs."],"tags":["claims","ddd","handoff","state-machine","typescript"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}