ruvnet/ruflo · error

Issue is not stealable or not claimed

Error message

Issue ${input.issueId} is not stealable or not claimed

What it means

Thrown by the steal handler in @claude-flow/claims when the target issue cannot be stolen: the handler requires an existing claim that is currently claimed AND already flagged stealable (claim.stealable === true). If the claim is missing, released, or the owner never marked it stealable, the steal is rejected with this combined message.

Solutions

  1. Have the owner mark the claim stealable first (mark-stealable with the owner's claimantId), then steal
  2. Confirm current state before stealing: claim exists, claimed by someone, stealable === true, and you are not already the owner
  3. Handle idempotency: if the steal already succeeded, treat 'not stealable or not claimed' as success when you now hold the claim

Example fix

// before
await stealTool({ issueId, claimantId: 'agent-2', claimantType: 'agent' });

// after
const claim = await getClaimTool({ issueId });
if (!claim.stealable && claim.claimantId !== 'agent-2') {
  await markStealableTool({ issueId, claimantId: claim.claimantId, reason: 'handoff' });
}
await stealTool({ issueId, claimantId: 'agent-2', claimantType: 'agent' });
Defensive patterns

Strategy: validation

Validate before calling

const claim = await getClaimTool({ issueId });
if (!claim || !claim.claimedBy || claim.claimedBy === claimantId) {
  return { stolen: false, reason: 'nothing to steal' };
}
if (!claim.stealable) {
  await markStealableTool({ issueId, claimantId: claim.claimantId, reason: 'unblocking' });
}
await stealTool({ issueId, claimantId, claimantType });

Try / catch

try { await stealTool(input); } catch (e) { if (e instanceof Error && e.message.includes('not stealable or not claimed')) { const c = await getClaimTool({ issueId: input.issueId }); if (c.claimantId === input.claimantId) return { stolen: true }; /* already ours */ throw e; } throw e; }

Prevention

When it happens

Trigger: Calling issue_steal directly on a normally-claimed issue (owner never ran mark-stealable); stealing after the claim was released or after a restart cleared the store; stealing twice — the second call finds the first steal already transferred ownership so the old claim no longer qualifies; using the wrong issueId.

Common situations: Eager workers trying to take over blocked tasks without the courtesy mark-stealable step; retry logic duplicating a steal that already succeeded; races between steal and release.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/5df2a753ec777987. Report an issue: GitHub.

Appendix: source

Thrown at v3/@claude-flow/claims/src/api/mcp-tools.ts:947

      // Update issue
      const issue = issueStore.get(input.issueId);
      if (issue) {
        issue.claimedBy = input.stealerId;
      }

      return {
        stolen: true,
        issueId: input.issueId,
        newClaimId,
        previousClaimant,
        contestWindowMs: 300000, // 5 minutes
        stolenAt,
      };
    }
  }

  throw new Error(`Issue ${input.issueId} is not stealable or not claimed`);
}

/**
 * Get stealable issues
 */
async function handleIssueGetStealable(
  input: z.infer<typeof issueGetStealableSchema>,
  context?: ToolContext
): Promise<{
  issues: Array<{
    issueId: string;
    title: string;
    priority: IssuePriority;
    currentClaimant: string;
    stealableReason?: string;
  }>;
  total: number;
}> {

View on GitHub (pinned to fa13ee4ad6)