{"record":{"id":"407e5f63e94b4269","repo":"ruvnet/ruflo","slug":"no-pending-handoff-for-issue-issueid","errorCode":null,"errorMessage":"No pending handoff for issue ${issueId}","messagePattern":"No pending handoff for issue (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/claim-service.ts","lineNumber":358,"sourceCode":"    claim.status = 'handoff-pending';\n    claim.statusChangedAt = new Date();\n    claim.handoffTo = to;\n    claim.handoffReason = reason;\n    await this.saveClaims();\n\n    this.emitEvent({\n      type: 'issue:handoff:requested',\n      timestamp: new Date(),\n      issueId,\n      claimant: from,\n      data: { to, reason },\n    });\n  }\n\n  async acceptHandoff(issueId: string, claimant: Claimant): Promise<void> {\n    const claim = this.claims.get(issueId);\n    if (!claim || claim.status !== 'handoff-pending') {\n      throw new Error(`No pending handoff for issue ${issueId}`);\n    }\n\n    if (!claim.handoffTo || !this.isSameClaimant(claim.handoffTo, claimant)) {\n      throw new Error(`Handoff not addressed to ${this.formatClaimant(claimant)}`);\n    }\n\n    const previousClaimant = claim.claimant;\n    claim.claimant = claimant;\n    claim.status = 'active';\n    claim.statusChangedAt = new Date();\n    delete claim.handoffTo;\n    delete claim.handoffReason;\n    await this.saveClaims();\n\n    this.emitEvent({\n      type: 'issue:handoff:accepted',\n      timestamp: new Date(),\n      issueId,","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/cli/src/services/claim-service.ts#L340-L376","documentation":"ClaimService.acceptHandoff(issueId, claimant) requires the issue's claim to exist AND be in status 'handoff-pending'. This error is thrown when either precondition fails: the issue has no claim at all, or its status is something else ('active', 'stealable', 'review-requested', ...) — i.e. no handoff is waiting to be accepted. Handoffs are a two-phase transfer: requestHandoff sets 'handoff-pending', acceptHandoff completes it.","triggerScenarios":"Calling acceptHandoff before requestHandoff was called; accepting twice (status already flipped back to 'active' by the first accept); accepting after the handoff was rejected (rejectHandoff restores 'active'); accepting after the claim was released or stolen in between.","commonSituations":"Both target agents processing the handoff notification concurrently (first wins, second throws), message redelivery/retries re-invoking accept, UI polling that re-fires the accept action.","solutions":["Pre-check state: const c = await service.getIssueStatus(issueId); only call accept when c?.status === 'handoff-pending'","Make acceptance single-shot per recipient: mark the notification consumed before/after accepting","If the status moved on, re-read the claim (getIssueStatus) and follow the current state instead of retrying blindly"],"exampleFix":"// before\nawait claims.acceptHandoff(issueId, workerB); // retried notification -> status already 'active'\n\n// after\nconst claim = await claims.getIssueStatus(issueId);\nif (claim?.status === 'handoff-pending' && sameClaimant(claim.handoffTo, workerB)) {\n  await claims.acceptHandoff(issueId, workerB);\n}","handlingStrategy":"try-catch","validationCode":"const claim = await claims.getIssueStatus(issueId);\nif (claim?.status === 'handoff-pending' && claim.handoffTo && isSameClaimant(claim.handoffTo, me)) {\n  await claims.acceptHandoff(issueId, me);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await claims.acceptHandoff(issueId, me);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('No pending handoff')) {\n    const c = await claims.getIssueStatus(issueId);\n    if (!c || c.status !== 'handoff-pending') return; // already resolved by someone — accept as done\n    throw e;\n  } else throw e;\n}","preventionTips":["Make handoff acceptance single-shot: consume the notification before/after a successful accept","Re-read claim status before every accept; handoffs are a two-phase state machine","Route each issue's handoff decisions through one handler so accept and reject cannot interleave"],"tags":["claim-service","handoff","state-machine","idempotency"],"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"}