Yeachan-Heo/oh-my-codex · error · Error

catalog_manifest_invalid:duplicate_skill:${name}

Error message

catalog_manifest_invalid:duplicate_skill:${name}

What it means

Skill names must be unique after trimming; the validator tracks names in a Set and throws catalog_manifest_invalid:duplicate_skill:<name> when the same trimmed name appears twice. The duplicate name is embedded in the message.

Source

Thrown at src/catalog/schema.ts:70

  if (!Array.isArray(input.skills)) throw new Error('catalog_manifest_invalid:skills');
  if (!Array.isArray(input.agents)) throw new Error('catalog_manifest_invalid:agents');

  const seenSkills = new Set<string>();
  const skills: CatalogSkillEntry[] = input.skills.map((entry, index) => {
    if (!isObject(entry)) throw new Error(`catalog_manifest_invalid:skills[${index}]`);
    assertNonEmptyString(entry.name, `skills[${index}].name`);
    assertNonEmptyString(entry.category, `skills[${index}].category`);
    assertNonEmptyString(entry.status, `skills[${index}].status`);

    if (!SKILL_CATEGORIES.has(entry.category as CatalogSkillCategory)) {
      throw new Error(`catalog_manifest_invalid:skills[${index}].category`);
    }
    if (!ENTRY_STATUSES.has(entry.status as CatalogEntryStatus)) {
      throw new Error(`catalog_manifest_invalid:skills[${index}].status`);
    }

    const name = entry.name.trim();
    if (seenSkills.has(name)) throw new Error(`catalog_manifest_invalid:duplicate_skill:${name}`);
    seenSkills.add(name);

    const canonical = typeof entry.canonical === 'string' && entry.canonical.trim() !== ''
      ? entry.canonical.trim()
      : undefined;

    if ((entry.status === 'alias' || entry.status === 'merged') && !canonical) {
      throw new Error(`catalog_manifest_invalid:skills[${index}].canonical`);
    }

    return {
      name,
      category: entry.category as CatalogSkillCategory,
      status: entry.status as CatalogEntryStatus,
      canonical,
      core: entry.core === true,
      internalRequired: entry.internalRequired === true,
    };

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Find both entries with the name in the message and remove or rename one
  2. If one should redirect to the other, mark the extra as status "alias" with a distinct name and canonical pointing at the primary
  3. Re-run the generator after deduplicating source data

Example fix

// before
"skills": [ {"name":"autopilot",...}, {"name":"autopilot",...} ]

// after
"skills": [ {"name":"autopilot",...}, {"name":"autopilot-classic","status":"alias","canonical":"autopilot","category":"execution"} ]
Defensive patterns

Strategy: validation

Validate before calling

const names = raw.skills.map((s: any) => String(s.name).trim());
if (new Set(names).size !== names.length) { /* dedupe before validation */ }

Prevention

When it happens

Trigger: Two entries in skills with the same name (including whitespace-only differences, since names are compared trimmed), e.g. two "autopilot" entries.

Common situations: Merging manifests from two sources, or an entry duplicated during refactoring from alias to active status.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/ce5d98a7a8e61195. Report an issue: GitHub.