{"record":{"id":"824cf393bfa60381","repo":"ruvnet/ruflo","slug":"issue-issueid-is-not-claimed-by-this-formatcl","errorCode":null,"errorMessage":"Issue ${issueId} is not claimed by ${this.formatClaimant(claimant)}","messagePattern":"Issue (.+?) is not claimed by (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/cli/src/services/claim-service.ts","lineNumber":306,"sourceCode":"    this.emitEvent({\n      type: 'issue:claimed',\n      timestamp: now,\n      issueId,\n      claimant,\n      previousClaimant: existing?.claimant,\n    });\n\n    return { success: true, claim };\n  }\n\n  async release(issueId: string, claimant: Claimant): Promise<void> {\n    const claim = this.claims.get(issueId);\n    if (!claim) {\n      throw new Error(`Issue ${issueId} is not claimed`);\n    }\n\n    if (!this.isSameClaimant(claim.claimant, claimant)) {\n      throw new Error(`Issue ${issueId} is not claimed by ${this.formatClaimant(claimant)}`);\n    }\n\n    this.claims.delete(issueId);\n    this.stealableInfo.delete(issueId);\n    await this.saveClaims();\n\n    this.emitEvent({\n      type: 'issue:released',\n      timestamp: new Date(),\n      issueId,\n      claimant,\n    });\n  }\n\n  // ==========================================================================\n  // Handoffs\n  // ==========================================================================\n","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/v3/@claude-flow/cli/src/services/claim-service.ts#L288-L324","documentation":"The second guard in release(): the claim exists, but isSameClaimant(claim.claimant, claimant) is false. isSameClaimant compares type plus userId (human) or agentId (agent). The message names the caller's claimant via formatClaimant (human:<name> or agent:<type>:<id>). The claim is not removed.","triggerScenarios":"Agent A trying to release a claim held by agent B; a human trying to release an agent's claim; using a stale claimant object whose agentId changed after a restart; case/whitespace drift in userId.","commonSituations":"Two agents with overlapping scopes; recovery logic using the wrong identity; tests using a default claimant that does not match the one used in claim(); renaming an agent without updating persisted claimant refs.","solutions":["Have each caller release only with the exact claimant it used to claim.","If a transfer is intended, use the handoff flow (requestHandoff/acceptHandoff) rather than release+claim.","Resolve identity drift: derive agentId from a stable source (not a random per-run id)."],"exampleFix":"// before: agent A releases B's claim\nawait claims.release(issueId, agentA); // claim held by agentB -> throws\n\n// after: only the holder releases, or use handoff\nconst claim = claims.peek?.(issueId);\nif (claim && isSameClaimant(claim.claimant, agentA)) {\n  await claims.release(issueId, agentA);\n} else if (claim) {\n  await claims.requestHandoff(issueId, claim.claimant, agentA, 'rebalance');\n}","handlingStrategy":"validation","validationCode":"function canRelease(claims, issueId, claimant) {\n  const c = claims.list?.().find(x => x.issueId === issueId);\n  return !!c && isSameClaimant(c.claimant, claimant);\n}","typeGuard":"function isSameClaimant(a, b): boolean {\n  if (a.type !== b.type) return false;\n  return a.type === 'human' ? a.userId === b.userId : a.agentId === b.agentId;\n}","tryCatchPattern":null,"preventionTips":["Always release with the exact claimant used to claim.","Use the handoff flow rather than release+claim for transfers.","Derive agentId/userId from stable sources, not per-run random ids."],"tags":["claim-service","ownership","authorization","state-machine"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}