{"record":{"id":"6f861e6024169f17","repo":"Yeachan-Heo/oh-my-codex","slug":"catalog-manifest-invalid-skills-index-canonica","errorCode":null,"errorMessage":"catalog_manifest_invalid:skills[${index}].canonical","messagePattern":"catalog_manifest_invalid:skills\\[(.+?)\\]\\.canonical","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/catalog/schema.ts","lineNumber":78,"sourceCode":"    assertNonEmptyString(entry.status, `skills[${index}].status`);\n\n    if (!SKILL_CATEGORIES.has(entry.category as CatalogSkillCategory)) {\n      throw new Error(`catalog_manifest_invalid:skills[${index}].category`);\n    }\n    if (!ENTRY_STATUSES.has(entry.status as CatalogEntryStatus)) {\n      throw new Error(`catalog_manifest_invalid:skills[${index}].status`);\n    }\n\n    const name = entry.name.trim();\n    if (seenSkills.has(name)) throw new Error(`catalog_manifest_invalid:duplicate_skill:${name}`);\n    seenSkills.add(name);\n\n    const canonical = typeof entry.canonical === 'string' && entry.canonical.trim() !== ''\n      ? entry.canonical.trim()\n      : undefined;\n\n    if ((entry.status === 'alias' || entry.status === 'merged') && !canonical) {\n      throw new Error(`catalog_manifest_invalid:skills[${index}].canonical`);\n    }\n\n    return {\n      name,\n      category: entry.category as CatalogSkillCategory,\n      status: entry.status as CatalogEntryStatus,\n      canonical,\n      core: entry.core === true,\n      internalRequired: entry.internalRequired === true,\n    };\n  });\n\n  const seenAgents = new Set<string>();\n  const agents: CatalogAgentEntry[] = input.agents.map((entry, index) => {\n    if (!isObject(entry)) throw new Error(`catalog_manifest_invalid:agents[${index}]`);\n    assertNonEmptyString(entry.name, `agents[${index}].name`);\n    assertNonEmptyString(entry.category, `agents[${index}].category`);\n    assertNonEmptyString(entry.status, `agents[${index}].status`);","sourceCodeStart":60,"sourceCodeEnd":96,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/catalog/schema.ts#L60-L96","documentation":"Entries whose status is alias or merged must declare a canonical skill name they redirect to; this error fires when such an entry has no canonical (or a whitespace-only one). Without canonical, alias/merged entries cannot be resolved to their target.","triggerScenarios":"A skills[i] entry with \"status\": \"alias\" or \"merged\" but missing the canonical field, or canonical: \"\" / \"   \".","commonSituations":"Renaming a skill to alias status and forgetting to add canonical, or generators that drop canonical when it equals the entry name.","solutions":["Add \"canonical\": \"<primary skill name>\" to the entry","Ensure the canonical name matches an existing non-alias skill name in the manifest","If the entry should stand alone, change status back to active"],"exampleFix":"// before\n{ \"name\": \"ap\", \"category\": \"execution\", \"status\": \"alias\" }\n\n// after\n{ \"name\": \"ap\", \"category\": \"execution\", \"status\": \"alias\", \"canonical\": \"autopilot\" }","handlingStrategy":"validation","validationCode":"for (const s of raw.skills) {\n  if ((s.status === 'alias' || s.status === 'merged') && !(typeof s.canonical === 'string' && s.canonical.trim())) {\n    /* add canonical or change status */\n  }\n}","typeGuard":"const hasCanonicalIfNeeded = (s: Record<string, unknown>) => !(s.status === 'alias' || s.status === 'merged') || (typeof s.canonical === 'string' && s.canonical.trim() !== '');","tryCatchPattern":null,"preventionTips":["When renaming a skill, immediately add canonical to the alias entry","Run validateCatalogManifest after every manifest edit"],"tags":["catalog","manifest","alias","validation","skills"],"backgroundTag":"missing-required-field","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}