Skip to content

Commit 4ed1fb6

Browse files
committed
fix: enforce thin legacy skill aliases [Story PRO-14.5]
1 parent 0ec1c66 commit 4ed1fb6

4 files changed

Lines changed: 103 additions & 6 deletions

File tree

.aiox-core/infrastructure/scripts/codex-skills-sync/validate.js

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,11 @@ function hasFullActivationPayload(content) {
277277
);
278278
}
279279

280-
function isIntentionalLegacyAlias(content, canonicalSkillId) {
280+
function escapeRegExp(value) {
281+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
282+
}
283+
284+
function hasLegacyAliasIntent(content, canonicalSkillId) {
281285
const value = String(content || '');
282286
const normalized = value.toLowerCase();
283287
return (
@@ -290,6 +294,41 @@ function isIntentionalLegacyAlias(content, canonicalSkillId) {
290294
);
291295
}
292296

297+
function getThinLegacyRedirectIssues(content, canonicalSkillId, legacySkillId) {
298+
const lines = String(content || '')
299+
.split(/\r?\n/)
300+
.map(line => line.trim())
301+
.filter(Boolean);
302+
const canonical = escapeRegExp(canonicalSkillId);
303+
const legacy = escapeRegExp(legacySkillId);
304+
const allowedLinePatterns = [
305+
/^<!--\s*AIOX-CODEX-LEGACY-ALIAS:\s*redirect\s*-->$/,
306+
new RegExp(`^#\\s+${legacy}$`),
307+
new RegExp(`^This legacy alias redirects to canonical skill \`${canonical}\`\\.$`),
308+
new RegExp(`^Use \`\\$?${canonical}\` instead\\.$`),
309+
];
310+
const issues = [];
311+
312+
if (!lines.some(line => /^<!--\s*AIOX-CODEX-LEGACY-ALIAS:\s*redirect\s*-->$/.test(line))) {
313+
issues.push('missing exact legacy redirect marker');
314+
}
315+
316+
if (!lines.some(line => new RegExp(`^This legacy alias redirects to canonical skill \`${canonical}\`\\.$`).test(line))) {
317+
issues.push(`missing exact canonical redirect sentence for "${canonicalSkillId}"`);
318+
}
319+
320+
const invalidLines = lines.filter(line => !allowedLinePatterns.some(pattern => pattern.test(line)));
321+
if (invalidLines.length > 0) {
322+
issues.push('contains non-redirect content');
323+
}
324+
325+
return issues;
326+
}
327+
328+
function isIntentionalLegacyAlias(content, canonicalSkillId, legacySkillId) {
329+
return getThinLegacyRedirectIssues(content, canonicalSkillId, legacySkillId).length === 0;
330+
}
331+
293332
function classifyLegacySkillAlias(content, item, relativeSkillPath) {
294333
if (!String(content || '').trim()) {
295334
return {
@@ -312,7 +351,7 @@ function classifyLegacySkillAlias(content, item, relativeSkillPath) {
312351
};
313352
}
314353

315-
if (isIntentionalLegacyAlias(content, item.skillId)) {
354+
if (isIntentionalLegacyAlias(content, item.skillId, item.legacySkillId)) {
316355
return {
317356
dir: item.legacySkillId,
318357
canonicalSkillId: item.skillId,
@@ -322,6 +361,17 @@ function classifyLegacySkillAlias(content, item, relativeSkillPath) {
322361
};
323362
}
324363

364+
if (hasLegacyAliasIntent(content, item.skillId)) {
365+
const issues = getThinLegacyRedirectIssues(content, item.skillId, item.legacySkillId);
366+
return {
367+
dir: item.legacySkillId,
368+
canonicalSkillId: item.skillId,
369+
classification: 'non-thin-legacy-alias',
370+
fatal: true,
371+
message: `Legacy skill alias is not a thin redirect: ${relativeSkillPath} -> ${item.skillId} (${issues.join('; ')})`,
372+
};
373+
}
374+
325375
return {
326376
dir: item.legacySkillId,
327377
canonicalSkillId: item.skillId,
@@ -545,6 +595,8 @@ module.exports = {
545595
extractGeneratedSquadSource,
546596
isGeneratedSquadSkill,
547597
hasFullActivationPayload,
598+
hasLegacyAliasIntent,
599+
getThinLegacyRedirectIssues,
548600
isIntentionalLegacyAlias,
549601
classifyLegacySkillAlias,
550602
parseArgs,

.aiox-core/install-manifest.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# - File types for categorization
99
#
1010
version: 5.1.17
11-
generated_at: "2026-05-10T00:55:53.386Z"
11+
generated_at: "2026-05-10T01:14:38.554Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1128
1414
files:
@@ -3101,9 +3101,9 @@ files:
31013101
type: script
31023102
size: 4072
31033103
- path: infrastructure/scripts/codex-skills-sync/validate.js
3104-
hash: sha256:0e1346c7093a882b838c480530366e280ff07df398e6335bca6762239d5cf486
3104+
hash: sha256:6c8f98f49e433fc3aa648034457d5273a2ca667b8c7e492157a89ed6d0582c96
31053105
type: script
3106-
size: 16006
3106+
size: 17947
31073107
- path: infrastructure/scripts/collect-tool-usage.js
31083108
hash: sha256:8a739b79182dc41e28b7e02aeb9ec1dde5ec49f3ca534399acc59711b3b92bbf
31093109
type: script

docs/migration/PRO-14.5-legacy-slash-command-shim-retirement.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Strict Codex skill validation must fail when any of these conditions appears:
3333
- an orphaned canonical `aiox-*` directory duplicates the full payload of a known source agent;
3434
- an unexpected `aiox-*` generated skill directory is orphaned from source agents, unless it is an explicitly detected generated squad skill.
3535

36-
Strict validation may pass with an intentional legacy alias only when the alias is a thin redirect and includes an explicit legacy-alias marker or equivalent canonical redirect text. Passing aliases are still reported as warnings so the migration remains visible.
36+
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.
3737

3838
## Removal Criteria
3939

tests/integration/codex-skills-sync.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,51 @@ describe('Codex Skills Sync', () => {
179179
expect(result.warnings.join('\n')).toContain('Intentional legacy skill alias directory');
180180
});
181181

182+
it('strict validation rejects legacy aliases that include extra non-redirect content', () => {
183+
const localSkillsDir = path.join(tmpRoot, '.codex', 'skills');
184+
syncSkills({
185+
sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'),
186+
localSkillsDir,
187+
dryRun: false,
188+
});
189+
190+
const legacyDir = path.join(localSkillsDir, 'aios-dev');
191+
fs.mkdirSync(legacyDir, { recursive: true });
192+
fs.writeFileSync(
193+
path.join(legacyDir, 'SKILL.md'),
194+
[
195+
'<!-- AIOX-CODEX-LEGACY-ALIAS: redirect -->',
196+
'# aios-dev',
197+
'',
198+
'This legacy alias redirects to canonical skill `aiox-dev`.',
199+
'',
200+
'When activated, load the developer persona and command list from this file.',
201+
'',
202+
].join('\n'),
203+
'utf8',
204+
);
205+
206+
const result = validateCodexSkills({
207+
sourceDir: path.join(process.cwd(), '.aiox-core', 'development', 'agents'),
208+
skillsDir: localSkillsDir,
209+
strict: true,
210+
});
211+
212+
expect(result.ok).toBe(false);
213+
expect(result.legacyAliases).toEqual(
214+
expect.arrayContaining([
215+
expect.objectContaining({
216+
dir: 'aios-dev',
217+
canonicalSkillId: 'aiox-dev',
218+
classification: 'non-thin-legacy-alias',
219+
fatal: true,
220+
}),
221+
]),
222+
);
223+
expect(result.errors.join('\n')).toContain('Legacy skill alias is not a thin redirect');
224+
expect(result.errors.join('\n')).toContain('contains non-redirect content');
225+
});
226+
182227
it('strict validation rejects orphaned canonical dirs that duplicate full skill payloads', () => {
183228
const localSkillsDir = path.join(tmpRoot, '.codex', 'skills');
184229
syncSkills({

0 commit comments

Comments
 (0)