diff --git a/.aiox-core/infrastructure/scripts/codex-skills-sync/index.js b/.aiox-core/infrastructure/scripts/codex-skills-sync/index.js index d75a6651f8..f6559806a3 100644 --- a/.aiox-core/infrastructure/scripts/codex-skills-sync/index.js +++ b/.aiox-core/infrastructure/scripts/codex-skills-sync/index.js @@ -42,12 +42,14 @@ function trimText(text, max = 220) { function getSkillId(agentId) { const id = String(agentId || '').trim(); if (id.startsWith('aiox-')) return id; + if (id.startsWith('aios-')) return id.replace(/^aios-/, 'aiox-'); return `aiox-${id}`; } function getLegacySkillId(agentId) { const id = String(agentId || '').trim(); if (!id) return 'aios-unknown'; + if (id.startsWith('aios-')) return id; if (id.startsWith('aiox-')) return id.replace(/^aiox-/, 'aios-'); return `aios-${id}`; } diff --git a/.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js b/.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js index 43f43184ab..e77c04f03d 100644 --- a/.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js +++ b/.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js @@ -246,6 +246,141 @@ function isGeneratedSquadSkill(content, projectRoot) { return fs.existsSync(path.join(projectRoot, sourcePath)); } +function readTextIfExists(filePath) { + try { + return fs.readFileSync(filePath, 'utf8'); + } catch (_error) { + return ''; + } +} + +function hasFullActivationPayload(content) { + const value = String(content || ''); + if (!value.trim()) { + return false; + } + + const frontmatter = parseSkillFrontmatter(value); + if (frontmatter.error) { + return false; + } + + const canonicalAgentPath = extractCanonicalAgentPath(value); + if (!canonicalAgentPath) { + return false; + } + + return ( + value.includes('## Activation Protocol') || + value.includes('generate-greeting.js') || + value.includes('source of truth') + ); +} + +function escapeRegExp(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function hasLegacyAliasIntent(content, canonicalSkillId) { + const value = String(content || ''); + const normalized = value.toLowerCase(); + return ( + value.includes('AIOX-CODEX-LEGACY-ALIAS') || + ( + normalized.includes('legacy alias') && + normalized.includes('canonical') && + value.includes(canonicalSkillId) + ) + ); +} + +function getThinLegacyRedirectIssues(content, canonicalSkillId, legacySkillId) { + const lines = String(content || '') + .split(/\r?\n/) + .map(line => line.trim()) + .filter(Boolean); + const canonical = escapeRegExp(canonicalSkillId); + const legacy = escapeRegExp(legacySkillId); + const allowedLinePatterns = [ + /^$/, + new RegExp(`^#\\s+${legacy}$`), + new RegExp(`^This legacy alias redirects to canonical skill \`${canonical}\`\\.$`), + new RegExp(`^Use \`\\$?${canonical}\` instead\\.$`), + ]; + const issues = []; + + if (!lines.some(line => /^$/.test(line))) { + issues.push('missing exact legacy redirect marker'); + } + + if (!lines.some(line => new RegExp(`^This legacy alias redirects to canonical skill \`${canonical}\`\\.$`).test(line))) { + issues.push(`missing exact canonical redirect sentence for "${canonicalSkillId}"`); + } + + const invalidLines = lines.filter(line => !allowedLinePatterns.some(pattern => pattern.test(line))); + if (invalidLines.length > 0) { + issues.push('contains non-redirect content'); + } + + return issues; +} + +function isIntentionalLegacyAlias(content, canonicalSkillId, legacySkillId) { + return getThinLegacyRedirectIssues(content, canonicalSkillId, legacySkillId).length === 0; +} + +function classifyLegacySkillAlias(content, item, relativeSkillPath) { + if (!String(content || '').trim()) { + return { + dir: item.legacySkillId, + canonicalSkillId: item.skillId, + classification: 'missing-skill-file', + fatal: true, + message: `Legacy skill alias directory missing SKILL.md: ${relativeSkillPath}`, + }; + } + + if (hasFullActivationPayload(content)) { + const canonicalAgentPath = extractCanonicalAgentPath(content); + return { + dir: item.legacySkillId, + canonicalSkillId: item.skillId, + classification: 'duplicate-full-payload', + fatal: true, + message: `Legacy skill alias duplicates full activation payload: ${relativeSkillPath} -> ${item.skillId} (${canonicalAgentPath})`, + }; + } + + if (isIntentionalLegacyAlias(content, item.skillId, item.legacySkillId)) { + return { + dir: item.legacySkillId, + canonicalSkillId: item.skillId, + classification: 'intentional-redirect', + fatal: false, + message: `Intentional legacy skill alias directory: ${relativeSkillPath} -> ${item.skillId}`, + }; + } + + if (hasLegacyAliasIntent(content, item.skillId)) { + const issues = getThinLegacyRedirectIssues(content, item.skillId, item.legacySkillId); + return { + dir: item.legacySkillId, + canonicalSkillId: item.skillId, + classification: 'non-thin-legacy-alias', + fatal: true, + message: `Legacy skill alias is not a thin redirect: ${relativeSkillPath} -> ${item.skillId} (${issues.join('; ')})`, + }; + } + + return { + dir: item.legacySkillId, + canonicalSkillId: item.skillId, + classification: 'unclassified-legacy-alias', + fatal: true, + message: `Unclassified legacy skill alias directory: ${relativeSkillPath}`, + }; +} + function validateCodexSkills(options = {}) { const defaults = getDefaultOptions(); const projectRoot = options.projectRoot || defaults.projectRoot; @@ -270,6 +405,8 @@ function validateCodexSkills(options = {}) { missing: [], orphaned: [], legacy: [], + legacyAliases: [], + duplicatePayloads: [], ignored: [], selfTests: [], }; @@ -307,8 +444,15 @@ function validateCodexSkills(options = {}) { const expectedIds = new Set(expected.map(item => item.skillId)); const legacyIds = new Set(expected.map(item => item.legacySkillId)); + const expectedByLegacyId = new Map(expected.map(item => [item.legacySkillId, item])); + const expectedByCanonicalPath = new Map(expected.map(item => [ + `.aiox-core/development/agents/${item.filename}`, + item, + ])); const orphaned = []; const legacy = []; + const legacyAliases = []; + const duplicatePayloads = []; const ignored = []; if (resolved.strict) { const dirs = fs.readdirSync(resolved.skillsDir, { withFileTypes: true }) @@ -316,8 +460,18 @@ function validateCodexSkills(options = {}) { .map(entry => entry.name); for (const dir of dirs) { if (legacyIds.has(dir)) { + const item = expectedByLegacyId.get(dir); + const skillPath = path.join(resolved.skillsDir, dir, 'SKILL.md'); + const relativeSkillPath = path.join(path.relative(resolved.projectRoot, resolved.skillsDir), dir, 'SKILL.md'); + const classification = classifyLegacySkillAlias(readTextIfExists(skillPath), item, relativeSkillPath); + legacy.push(dir); - errors.push(`Legacy skill alias directory: ${path.join(path.relative(resolved.projectRoot, resolved.skillsDir), dir)}`); + legacyAliases.push(classification); + if (classification.fatal) { + errors.push(classification.message); + } else { + warnings.push(classification.message); + } continue; } if (dir.startsWith('aiox-') && !expectedIds.has(dir)) { @@ -332,6 +486,20 @@ function validateCodexSkills(options = {}) { content = ''; } + const canonicalAgentPath = extractCanonicalAgentPath(content); + const duplicateOf = canonicalAgentPath ? expectedByCanonicalPath.get(canonicalAgentPath) : null; + if (duplicateOf && hasFullActivationPayload(content)) { + duplicatePayloads.push({ + dir, + canonicalSkillId: duplicateOf.skillId, + canonicalAgentPath, + }); + errors.push( + `Duplicate full skill payload: ${path.join(path.relative(resolved.projectRoot, resolved.skillsDir), dir, 'SKILL.md')} -> ${duplicateOf.skillId} (${canonicalAgentPath})`, + ); + continue; + } + if (isGeneratedSquadSkill(content, resolved.projectRoot)) { ignored.push(dir); continue; @@ -370,6 +538,8 @@ function validateCodexSkills(options = {}) { missing, orphaned, legacy, + legacyAliases, + duplicatePayloads, ignored, selfTests, }; @@ -424,6 +594,11 @@ module.exports = { runSkillSelfTests, extractGeneratedSquadSource, isGeneratedSquadSkill, + hasFullActivationPayload, + hasLegacyAliasIntent, + getThinLegacyRedirectIssues, + isIntentionalLegacyAlias, + classifyLegacySkillAlias, parseArgs, getDefaultOptions, }; diff --git a/.aiox-core/install-manifest.yaml b/.aiox-core/install-manifest.yaml index 5e25e2d75d..b9fbed98b5 100644 --- a/.aiox-core/install-manifest.yaml +++ b/.aiox-core/install-manifest.yaml @@ -8,7 +8,7 @@ # - File types for categorization # version: 5.1.17 -generated_at: "2026-05-09T09:07:47.281Z" +generated_at: "2026-05-10T01:14:38.554Z" generator: scripts/generate-install-manifest.js file_count: 1128 files: @@ -3093,17 +3093,17 @@ files: type: script size: 22355 - path: infrastructure/scripts/codex-skills-sync/index.js - hash: sha256:caf8e4f6fad8d5b5c123bf47561a632bdb2cfacb5f3a80265de81ba0dea3f6b9 + hash: sha256:2dc8637ba0095921e3cb5e99791dc05da61a12e375407b05f199696bcce7ea61 type: script - size: 5533 + size: 5642 - path: infrastructure/scripts/codex-skills-sync/README.md hash: sha256:2e4366ccc31b617a9be8a8b19c0379e1fd3ae64c4d5cfbb4d233860f9db0f949 type: script size: 4072 - path: infrastructure/scripts/codex-skills-sync/validate.js - hash: sha256:5b8cb91ddcfda2b6b66328c12129e71665e4e1cb6f13b1792746e9d761d1ebcf + hash: sha256:6c8f98f49e433fc3aa648034457d5273a2ca667b8c7e492157a89ed6d0582c96 type: script - size: 12185 + size: 17947 - path: infrastructure/scripts/collect-tool-usage.js hash: sha256:8a739b79182dc41e28b7e02aeb9ec1dde5ec49f3ca534399acc59711b3b92bbf type: script diff --git a/.codex/skills/aiox-master/SKILL.md b/.codex/skills/aiox-master/SKILL.md index 89eba89db9..1905ddd21a 100644 --- a/.codex/skills/aiox-master/SKILL.md +++ b/.codex/skills/aiox-master/SKILL.md @@ -19,10 +19,10 @@ Use when you need comprehensive expertise across all domains, framework componen - `*kb` - Toggle KB mode (loads AIOX Method knowledge) - `*status` - Show current context and progress - `*guide` - Show comprehensive usage guide for this agent -- `*exit` - Exit agent mode - `*create` - Create new AIOX component (agent, task, workflow, template, checklist) - `*modify` - Modify existing AIOX component -- `*update-manifest` - Update team manifest +- `*task` - Execute specific task (or list available) +- `*workflow` - Start workflow (guided=manual, engine=real subagent spawning) ## Non-Negotiables - Follow `.aiox-core/constitution.md`. diff --git a/docs/migration/PRO-14.5-legacy-slash-command-shim-retirement.md b/docs/migration/PRO-14.5-legacy-slash-command-shim-retirement.md new file mode 100644 index 0000000000..49e5002db7 --- /dev/null +++ b/docs/migration/PRO-14.5-legacy-slash-command-shim-retirement.md @@ -0,0 +1,86 @@ +# PRO-14.5 Legacy Slash-Command Shim Retirement Gate + +Story: `STORY-PRO-14.5` +Date: 2026-05-09 +Repo: `aiox-core` +Status: implementation gate + +## Purpose + +This document defines the retirement gate for legacy AIOS-era slash-command shims and `aios-*` skill aliases. It does not authorize deletion. It makes deletion possible later only after generated surfaces prove canonical AIOX skills are unique, validation blocks duplicate payloads, supported IDE projections pass, and the compatibility window is approved. + +## Inventory + +| Surface | Current classification | Evidence | Retirement posture | +|---|---|---|---| +| `.aiox-core/infrastructure/scripts/codex-skills-sync/index.js` | canonical payload generator + legacy id normalizer | Generates `aiox-*` skill ids and now normalizes `aios-*` inputs to `aiox-*`. | Keep. This is the source of canonical Codex skill ids. | +| `.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js` | strict validator + legacy alias detector | Strict mode reports legacy aliases, rejects full duplicate payloads and rejects duplicate orphaned canonical payloads. | Keep as blocking gate before any alias removal. | +| `.codex/skills/` | generated projection | Observed 13 `aiox-*` skill dirs and 0 `aios-*` dirs in this worktree. | Canonical projection. Regenerate with `npm run sync:skills:codex`. | +| `.claude/commands/AIOX/agents/*.md` | legacy slash-command shims | 12 command files contain `ACORE-CLAUDE-AGENT-COMMAND: legacy-shim` and redirect to `.claude/skills/AIOX/agents/*/SKILL.md`. | Keep until usage cutoff or telemetry evidence approves removal. | +| `.claude/skills/AIOX/agents/*/SKILL.md` | canonical Claude skill payloads | 12 skill dirs observed. | Canonical Claude activation payloads. | +| `.claude/skills/AIOX/agents/aiox-master/SKILL.md` | canonical payload with current double-prefix frontmatter | Current generated frontmatter contains `name: aiox-aiox-master`. | Classify as existing contract; do not change without a separate focused compatibility story. | +| `.aiox-core/infrastructure/scripts/ide-sync/transformers/kimi.js` | canonical Kimi generator + legacy id normalizer | `aios-*` preferred aliases normalize to `aiox-*`; tests cover this. | Keep. | +| `.kimi/skills/` | generated projection | Observed 12 `aiox-*` skill dirs and 0 `aios-*` dirs. | Canonical projection. | +| `.aiox-core/infrastructure/scripts/ide-sync/gemini-commands.js` | canonical Gemini command generator | Generates `aiox-{slug}.toml` and `aiox-menu.toml`. | Keep. | +| `.gemini/commands/` | generated projection | Observed 13 `aiox-*` command files, including `aiox-menu.toml`, and 0 `aios-*` files. | Canonical projection. | +| `sinkra-hub/apps/gateway-ai/hooks/aios-command` | external legacy consumer | Existing package/hook name and logs still use `aios-command`. | Do not change in this story; open an owning-repo story before migration. | + +## Blocking Rules + +Strict Codex skill validation must fail when any of these conditions appears: + +- a legacy `aios-*` directory contains a full activation payload instead of a classified redirect; +- an orphaned canonical `aiox-*` directory duplicates the full payload of a known source agent; +- an unexpected `aiox-*` generated skill directory is orphaned from source agents, unless it is an explicitly detected generated squad skill. + +Strict validation may pass with an intentional legacy alias only when the alias is a thin redirect with the exact `AIOX-CODEX-LEGACY-ALIAS: redirect` marker and an explicit canonical redirect sentence. Any extra non-redirect content is fatal. Passing aliases are still reported as warnings so the migration remains visible. + +## Removal Criteria + +Legacy slash-command shims or `aios-*` alias dirs may only be removed after all criteria below are true: + +1. `npm run sync:skills:codex` produces no unexpected generated diff. +2. `npm run validate:codex-skills:self-test` passes. +3. `npm run validate:codex-sync` and `npm run validate:codex-integration` pass. +4. `npm run sync:ide:check` passes for enabled IDE targets. +5. `npm run validate:gemini-sync` and `npm run validate:gemini-integration` pass. +6. The generated-surface inventory shows one canonical payload per supported agent per IDE surface. +7. Legacy shim usage is either measured as zero for one full minor release or covered by a documented support cutoff. +8. `@architect`, `@qa` and `@po` approve the removal story before deletion. + +## Compatibility Window + +Default window: keep legacy command shims and legacy alias recognition for at least one full minor release after this validation gate lands. If no telemetry exists for a specific shim surface, the removal story must provide a dated support cutoff and rollback plan before deletion. + +## Validation Evidence + +Validated on 2026-05-09 from branch `feat/pro-14-5-legacy-shim-retirement`: + +| Command | Result | Notes | +|---|---|---| +| `npm ci` | Pass | Installed dependencies cleanly; transient executable mode drift from install was reverted before commit. | +| `npm run sync:skills:codex` | Pass | Generated 12 Codex skills. | +| `npm run validate:codex-skills:self-test` | Pass | 12 skills checked; 12 self-tests passed. | +| `npm run validate:codex-sync` | Pass | expected 12, synced 12, missing 0, drift 0, orphaned 0. | +| `npm run validate:codex-integration` | Pass with warning | Warns that generated squad skill `aiox-claude-mastery-chief` makes Codex skill count 13/12; existing known generated extra, not a duplicate legacy payload. | +| `npm run sync:ide:check` | Pass | expected 109, synced 109, missing 0, drift 0, orphaned 0. | +| `npm run validate:gemini-sync` | Pass | expected 25, synced 25, missing 0, drift 0, orphaned 0. | +| `npm run validate:gemini-integration` | Pass with warning | Warns `.gemini/rules.md` is not present; existing integration warning. | +| `npm run lint` | Pass | Re-run after full tests released temporary fixture state. | +| `npm run typecheck` | Pass | No TypeScript errors. | +| `npm test -- --runInBand --forceExit` | Pass | 341 suites passed, 12 skipped; 8433 tests passed, 172 skipped. `--forceExit` used because the exact command kept open handles after completion. | +| `npm run build` | Unavailable | Package has no `build` script; `npm run validate:publish` was run as the package publication gate instead. | +| `npm run validate:publish` | Pass | Package contents, dependency completeness and publish safety gate passed. | +| `git diff --check` | Pass | No whitespace errors. | +| `rg -n 'aios-\|aiox-\|slash-command\|aios-command\|legacy-shim' .aiox-core .claude .codex .gemini .kimi packages tests docs -S` | Pass | Inventory command completed; expected references are documented above. | + +## Rollback + +If removal later breaks activation, rollback is: + +1. Restore `.claude/commands/AIOX/agents/*.md` from the previous generator output or git history. +2. Restore any removed legacy `aios-*` alias dirs as thin redirects, not full duplicated payloads. +3. Run `npm run sync:skills:codex`. +4. Run `npm run sync:ide`. +5. Run all validation commands listed in the removal story. +6. Publish a patch release only after validation passes. diff --git a/tests/integration/codex-skills-sync.test.js b/tests/integration/codex-skills-sync.test.js index 222be08617..78936c777f 100644 --- a/tests/integration/codex-skills-sync.test.js +++ b/tests/integration/codex-skills-sync.test.js @@ -7,8 +7,18 @@ const path = require('path'); const { syncSkills, buildSkillContent, + getSkillId, getLegacySkillId, -} = require('../../.aiox-core/infrastructure/scripts/codex-skills-sync/index'); +} = require(path.join( + process.cwd(), + '.aiox-core/infrastructure/scripts/codex-skills-sync/index', +)); +const { + validateCodexSkills, +} = require(path.join( + process.cwd(), + '.aiox-core/infrastructure/scripts/codex-skills-sync/validate', +)); describe('Codex Skills Sync', () => { let tmpRoot; @@ -92,7 +102,162 @@ describe('Codex Skills Sync', () => { }); it('derives legacy aliases for migrated core agents', () => { + expect(getSkillId('aios-dev')).toBe('aiox-dev'); expect(getLegacySkillId('dev')).toBe('aios-dev'); expect(getLegacySkillId('aiox-master')).toBe('aios-master'); + expect(getLegacySkillId('aios-master')).toBe('aios-master'); + }); + + it('strict validation rejects legacy aliases that duplicate full skill payloads', () => { + const localSkillsDir = path.join(tmpRoot, '.codex', 'skills'); + syncSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + localSkillsDir, + dryRun: false, + }); + + const canonicalDir = path.join(localSkillsDir, 'aiox-dev'); + const legacyDir = path.join(localSkillsDir, 'aios-dev'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.copyFileSync(path.join(canonicalDir, 'SKILL.md'), path.join(legacyDir, 'SKILL.md')); + + const result = validateCodexSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + skillsDir: localSkillsDir, + strict: true, + }); + + expect(result.ok).toBe(false); + expect(result.legacy).toContain('aios-dev'); + expect(result.legacyAliases).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + dir: 'aios-dev', + canonicalSkillId: 'aiox-dev', + classification: 'duplicate-full-payload', + fatal: true, + }), + ]), + ); + expect(result.errors.join('\n')).toContain('Legacy skill alias duplicates full activation payload'); + }); + + it('strict validation reports intentional legacy aliases without treating them as duplicate payloads', () => { + const localSkillsDir = path.join(tmpRoot, '.codex', 'skills'); + syncSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + localSkillsDir, + dryRun: false, + }); + + const legacyDir = path.join(localSkillsDir, 'aios-dev'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync( + path.join(legacyDir, 'SKILL.md'), + [ + '', + '# aios-dev', + '', + 'This legacy alias redirects to canonical skill `aiox-dev`.', + '', + ].join('\n'), + 'utf8', + ); + + const result = validateCodexSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + skillsDir: localSkillsDir, + strict: true, + }); + + expect(result.ok).toBe(true); + expect(result.legacy).toContain('aios-dev'); + expect(result.legacyAliases).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + dir: 'aios-dev', + canonicalSkillId: 'aiox-dev', + classification: 'intentional-redirect', + fatal: false, + }), + ]), + ); + expect(result.warnings.join('\n')).toContain('Intentional legacy skill alias directory'); + }); + + it('strict validation rejects legacy aliases that include extra non-redirect content', () => { + const localSkillsDir = path.join(tmpRoot, '.codex', 'skills'); + syncSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + localSkillsDir, + dryRun: false, + }); + + const legacyDir = path.join(localSkillsDir, 'aios-dev'); + fs.mkdirSync(legacyDir, { recursive: true }); + fs.writeFileSync( + path.join(legacyDir, 'SKILL.md'), + [ + '', + '# aios-dev', + '', + 'This legacy alias redirects to canonical skill `aiox-dev`.', + '', + 'When activated, load the developer persona and command list from this file.', + '', + ].join('\n'), + 'utf8', + ); + + const result = validateCodexSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + skillsDir: localSkillsDir, + strict: true, + }); + + expect(result.ok).toBe(false); + expect(result.legacyAliases).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + dir: 'aios-dev', + canonicalSkillId: 'aiox-dev', + classification: 'non-thin-legacy-alias', + fatal: true, + }), + ]), + ); + expect(result.errors.join('\n')).toContain('Legacy skill alias is not a thin redirect'); + expect(result.errors.join('\n')).toContain('contains non-redirect content'); + }); + + it('strict validation rejects orphaned canonical dirs that duplicate full skill payloads', () => { + const localSkillsDir = path.join(tmpRoot, '.codex', 'skills'); + syncSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + localSkillsDir, + dryRun: false, + }); + + const duplicateDir = path.join(localSkillsDir, 'aiox-dev-copy'); + fs.mkdirSync(duplicateDir, { recursive: true }); + fs.copyFileSync(path.join(localSkillsDir, 'aiox-dev', 'SKILL.md'), path.join(duplicateDir, 'SKILL.md')); + + const result = validateCodexSkills({ + sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'), + skillsDir: localSkillsDir, + strict: true, + }); + + expect(result.ok).toBe(false); + expect(result.duplicatePayloads).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + dir: 'aiox-dev-copy', + canonicalSkillId: 'aiox-dev', + canonicalAgentPath: '.aiox-core/development/agents/dev.md', + }), + ]), + ); + expect(result.errors.join('\n')).toContain('Duplicate full skill payload'); }); }); diff --git a/tests/unit/codex-skills-validate.test.js b/tests/unit/codex-skills-validate.test.js index 74538277af..75b38bf4ad 100644 --- a/tests/unit/codex-skills-validate.test.js +++ b/tests/unit/codex-skills-validate.test.js @@ -168,6 +168,8 @@ describe('Codex Skills Validator', () => { expect(result.missing).toEqual([]); expect(result.orphaned).toEqual([]); expect(result.legacy).toEqual([]); + expect(result.legacyAliases).toEqual([]); + expect(result.duplicatePayloads).toEqual([]); expect(result.ignored).toEqual([]); expect(result.selfTests).toEqual([]); }); @@ -221,7 +223,16 @@ describe('Codex Skills Validator', () => { expect(result.ok).toBe(false); expect(result.legacy).toContain('aios-dev'); - expect(result.errors.some(error => error.includes('Legacy skill alias directory'))).toBe(true); + expect(result.legacyAliases).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + dir: 'aios-dev', + classification: 'unclassified-legacy-alias', + fatal: true, + }), + ]), + ); + expect(result.errors.some(error => error.includes('Unclassified legacy skill alias directory'))).toBe(true); }); it('ignores generated squad chief skills that point to an existing squad source', () => {