affaan-m/ECC · error · Error

assigneeKind must be 'agent' or 'human'.

Error message

assigneeKind must be 'agent' or 'human'.

What it means

Thrown by claimWorkItem when assigneeKind is provided but is not one of VALID_ASSIGNEE_KINDS ('agent' or 'human'). The kind is lowercased before checking, so case differences are tolerated, but any other value (e.g. 'bot', 'system', 'auto') is rejected because the board only distinguishes the two kinds. Leaving assigneeKind undefined is allowed and skips the metadata tag.

Source

Thrown at scripts/lib/control-pane/work-item-mutations.js:62

    }
    return item;
  }
  const { items } = store.listWorkItems({ limit: 100 });
  return items.filter(item => !item.owner && isOpenStatus(item.status)).sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority))[0] || null;
}

/**
 * Claim an unassigned work item for an agent or human. Sets the owner (and
 * optional assigneeKind) and moves the card to running unless an explicit
 * status is supplied. Returns { claimed, item } or { claimed: false, reason }.
 */
function claimWorkItem(store, { id, owner, assigneeKind, sessionId, status } = {}) {
  if (!owner) {
    throw new Error('claim requires an owner.');
  }
  const kind = assigneeKind ? String(assigneeKind).toLowerCase() : null;
  if (kind && !VALID_ASSIGNEE_KINDS.has(kind)) {
    throw new Error("assigneeKind must be 'agent' or 'human'.");
  }
  const target = selectClaimTarget(store, { id });
  if (!target) {
    return { claimed: false, reason: 'no-unassigned-open-items' };
  }
  if (!isOpenStatus(target.status)) {
    throw new Error(`Work item ${target.id} is already done; cannot claim.`);
  }
  const metadata = { ...(target.metadata || {}) };
  if (kind) {
    metadata.assigneeKind = kind;
  }
  const item = store.upsertWorkItem({
    ...target,
    owner,
    sessionId: sessionId ?? target.sessionId ?? null,
    status: status ?? 'running',
    metadata,

View on GitHub (pinned to 01e15490f0)

Solutions

  1. Pass only 'agent', 'human', or undefined/omitted for assigneeKind.
  2. Map external role names to one of the two valid values before calling claimWorkItem.
  3. If a new kind is genuinely needed, extend VALID_ASSIGNEE_KINDS in work-item-mutations.js and update the board rendering.
  4. Validate at the call site: const kind = ['agent','human'].includes(String(raw).toLowerCase()) ? String(raw).toLowerCase() : undefined.

Example fix

// before
claimWorkItem(store, { id, owner, assigneeKind: role }); // role may be 'bot'

// after — coerce to a valid kind or omit
const raw = String(role || '').trim().toLowerCase();
const assigneeKind = raw === 'agent' || raw === 'human' ? raw : undefined;
claimWorkItem(store, { id, owner, assigneeKind });
Defensive patterns

Strategy: type-guard

Validate before calling

const raw = String(assigneeKind || '').trim().toLowerCase();
if (raw && raw !== 'agent' && raw !== 'human') {
  throw new Error(`assigneeKind must be 'agent' or 'human', got ${assigneeKind}`);
}

Type guard

function isValidAssigneeKind(kind) {
  const k = String(kind || '').trim().toLowerCase();
  return k === '' || k === 'agent' || k === 'human';
}

Try / catch

try {
  claimWorkItem(store, { id, owner, assigneeKind });
} catch (e) {
  if (/assigneeKind must be/.test(e.message)) { claimWorkItem(store, { id, owner }); return; }
  throw e;
}

Prevention

When it happens

Trigger: Passing assigneeKind: 'bot' or 'system'; passing a value from an external system that uses different role names; a typo like 'agents' or 'Human ' with trailing space (trim is applied to lane but the kind check uses the lowercased value without trim — note the code does String(assigneeKind).toLowerCase() but NOT trim).

Common situations: Integrating an external tracker whose role taxonomy differs; user-typed kind in a CLI/UI; copied config from another tool.

Related errors


AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13). Data as JSON: /api/errors/19e8196a3ac3f197. Report an issue: GitHub.