affaan-m/ECC · error

Issue # is already claimed by

Error message

Issue #${issue.number} is already claimed by ${state.owner || 'unknown'}

What it means

assertIssueClaimable throws this when the coordination state for an issue has status 'claimed', meaning another agent/process already owns the GitHub issue. The claim-lock mechanism prevents duplicate concurrent work on the same issue; applyClaim calls this check before assigning a new owner. The existing owner is embedded in the message (or 'unknown' if state.owner is empty).

Solutions

  1. Pick a different, unclaimed issue from the queue instead of contending for this one.
  2. Contact the listed owner to confirm the claim is still active; if abandoned, have the owner release the claim (reset status in the coordination state) and retry.
  3. Inspect scripts/lib/github-coordination state file for the issue to verify the owner and timestamp; clear stale claims only after confirming the owner is inactive.
  4. Add a retry loop that re-reads state before claiming to reduce races between simultaneous claim attempts.

Example fix

// before: blindly claim
await applyClaim(repo, issueNumber, myOwner);

// after: check first
const state = readIssueState(repo, issueNumber);
if (state.status === 'claimed') {
  console.log(`Skipping #${issueNumber}: owned by ${state.owner}`);
} else {
  await applyClaim(repo, issueNumber, myOwner);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const state = readIssueState(repo, issueNumber);
if (state && state.status === 'claimed') {
  throw new SkipError(`#${issueNumber} claimed by ${state.owner}`);
}

Type guard

function isClaimable(state) {
  return state != null && typeof state === 'object' && state.status !== 'claimed';
}

Try / catch

try {
  await applyClaim(repo, issueNumber, owner);
} catch (e) {
  if (/is already claimed by/.test(e.message)) {
    console.warn(`Skipping: ${e.message}`);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling applyClaim(repo, issueNumber, owner) while state.js on disk already records { status: 'claimed', owner: <someone> } for that issue; two agents attempting to claim the same issue simultaneously; a stale claim from a previous session that was never released.

Common situations: Parallel CI jobs or multiple agents competing for issues from the same queue; a crashed session left the issue marked claimed; a developer locally re-runs a claim script after another teammate already claimed the issue.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/d8ca6802d84bcc13. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/github-coordination/state.js:215

    validation: state.validation || 'pending',
    review: state.review || 'not-requested',
    project: summarizeProjectProjection(state, policy),
    dependencies: Array.isArray(state.dependencies) ? state.dependencies : [],
    tasks: Array.isArray(state.tasks) ? state.tasks : [],
    labels: normalizeLabels(issue.labels),
    workItemId: `github-${slugifySegment(repo)}-epic-${issue.number}`,
    lastActionAt: state.lastActionAt || null,
    lastSyncAt: state.lastSyncAt || null,
  };
}

function assertIssueClaimable(issue, state) {
  if (String(issue.state || '').toLowerCase() !== 'open') {
    throw new Error(`Issue #${issue.number} is not open`);
  }

  if (state.status === 'claimed') {
    throw new Error(`Issue #${issue.number} is already claimed by ${state.owner || 'unknown'}`);
  }
}

function verifyDependenciesClosed(repo, dependencyNumbers, options = {}, allIssues = null) {
  if (!Array.isArray(dependencyNumbers) || dependencyNumbers.length === 0) {
    return [];
  }

  const issueList = allIssues || listIssues(repo, { ...options, state: 'all', limit: options.limit || 200 });
  const closed = [];
  for (const dependencyNumber of dependencyNumbers) {
    const issue = findIssueByNumber(issueList, dependencyNumber);
    if (!issue) {
      process.stderr.write(`[github-coordination] Warning: dependency issue #${dependencyNumber} not found in issue list (may be in a different repo or beyond limit)\n`);
    } else if (String(issue.state || '').toLowerCase() === 'closed') {
      closed.push(dependencyNumber);
    }
  }

View on GitHub (pinned to 8321021c54)