Skip to content

Commit c25b66e

Browse files
authored
fix: harden legacy skill retirement gate [Story PRO-14.5] (#723)
* fix: harden legacy skill retirement gate [Story PRO-14.5] * fix: enforce thin legacy skill aliases [Story PRO-14.5] * test: align codex skill imports [Story PRO-14.5]
1 parent 27af655 commit c25b66e

7 files changed

Lines changed: 449 additions & 10 deletions

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,14 @@ function trimText(text, max = 220) {
4242
function getSkillId(agentId) {
4343
const id = String(agentId || '').trim();
4444
if (id.startsWith('aiox-')) return id;
45+
if (id.startsWith('aios-')) return id.replace(/^aios-/, 'aiox-');
4546
return `aiox-${id}`;
4647
}
4748

4849
function getLegacySkillId(agentId) {
4950
const id = String(agentId || '').trim();
5051
if (!id) return 'aios-unknown';
52+
if (id.startsWith('aios-')) return id;
5153
if (id.startsWith('aiox-')) return id.replace(/^aiox-/, 'aios-');
5254
return `aios-${id}`;
5355
}

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

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,141 @@ function isGeneratedSquadSkill(content, projectRoot) {
246246
return fs.existsSync(path.join(projectRoot, sourcePath));
247247
}
248248

249+
function readTextIfExists(filePath) {
250+
try {
251+
return fs.readFileSync(filePath, 'utf8');
252+
} catch (_error) {
253+
return '';
254+
}
255+
}
256+
257+
function hasFullActivationPayload(content) {
258+
const value = String(content || '');
259+
if (!value.trim()) {
260+
return false;
261+
}
262+
263+
const frontmatter = parseSkillFrontmatter(value);
264+
if (frontmatter.error) {
265+
return false;
266+
}
267+
268+
const canonicalAgentPath = extractCanonicalAgentPath(value);
269+
if (!canonicalAgentPath) {
270+
return false;
271+
}
272+
273+
return (
274+
value.includes('## Activation Protocol') ||
275+
value.includes('generate-greeting.js') ||
276+
value.includes('source of truth')
277+
);
278+
}
279+
280+
function escapeRegExp(value) {
281+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
282+
}
283+
284+
function hasLegacyAliasIntent(content, canonicalSkillId) {
285+
const value = String(content || '');
286+
const normalized = value.toLowerCase();
287+
return (
288+
value.includes('AIOX-CODEX-LEGACY-ALIAS') ||
289+
(
290+
normalized.includes('legacy alias') &&
291+
normalized.includes('canonical') &&
292+
value.includes(canonicalSkillId)
293+
)
294+
);
295+
}
296+
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+
332+
function classifyLegacySkillAlias(content, item, relativeSkillPath) {
333+
if (!String(content || '').trim()) {
334+
return {
335+
dir: item.legacySkillId,
336+
canonicalSkillId: item.skillId,
337+
classification: 'missing-skill-file',
338+
fatal: true,
339+
message: `Legacy skill alias directory missing SKILL.md: ${relativeSkillPath}`,
340+
};
341+
}
342+
343+
if (hasFullActivationPayload(content)) {
344+
const canonicalAgentPath = extractCanonicalAgentPath(content);
345+
return {
346+
dir: item.legacySkillId,
347+
canonicalSkillId: item.skillId,
348+
classification: 'duplicate-full-payload',
349+
fatal: true,
350+
message: `Legacy skill alias duplicates full activation payload: ${relativeSkillPath} -> ${item.skillId} (${canonicalAgentPath})`,
351+
};
352+
}
353+
354+
if (isIntentionalLegacyAlias(content, item.skillId, item.legacySkillId)) {
355+
return {
356+
dir: item.legacySkillId,
357+
canonicalSkillId: item.skillId,
358+
classification: 'intentional-redirect',
359+
fatal: false,
360+
message: `Intentional legacy skill alias directory: ${relativeSkillPath} -> ${item.skillId}`,
361+
};
362+
}
363+
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+
375+
return {
376+
dir: item.legacySkillId,
377+
canonicalSkillId: item.skillId,
378+
classification: 'unclassified-legacy-alias',
379+
fatal: true,
380+
message: `Unclassified legacy skill alias directory: ${relativeSkillPath}`,
381+
};
382+
}
383+
249384
function validateCodexSkills(options = {}) {
250385
const defaults = getDefaultOptions();
251386
const projectRoot = options.projectRoot || defaults.projectRoot;
@@ -270,6 +405,8 @@ function validateCodexSkills(options = {}) {
270405
missing: [],
271406
orphaned: [],
272407
legacy: [],
408+
legacyAliases: [],
409+
duplicatePayloads: [],
273410
ignored: [],
274411
selfTests: [],
275412
};
@@ -307,17 +444,34 @@ function validateCodexSkills(options = {}) {
307444

308445
const expectedIds = new Set(expected.map(item => item.skillId));
309446
const legacyIds = new Set(expected.map(item => item.legacySkillId));
447+
const expectedByLegacyId = new Map(expected.map(item => [item.legacySkillId, item]));
448+
const expectedByCanonicalPath = new Map(expected.map(item => [
449+
`.aiox-core/development/agents/${item.filename}`,
450+
item,
451+
]));
310452
const orphaned = [];
311453
const legacy = [];
454+
const legacyAliases = [];
455+
const duplicatePayloads = [];
312456
const ignored = [];
313457
if (resolved.strict) {
314458
const dirs = fs.readdirSync(resolved.skillsDir, { withFileTypes: true })
315459
.filter(entry => entry.isDirectory() && (entry.name.startsWith('aiox-') || entry.name.startsWith('aios-')))
316460
.map(entry => entry.name);
317461
for (const dir of dirs) {
318462
if (legacyIds.has(dir)) {
463+
const item = expectedByLegacyId.get(dir);
464+
const skillPath = path.join(resolved.skillsDir, dir, 'SKILL.md');
465+
const relativeSkillPath = path.join(path.relative(resolved.projectRoot, resolved.skillsDir), dir, 'SKILL.md');
466+
const classification = classifyLegacySkillAlias(readTextIfExists(skillPath), item, relativeSkillPath);
467+
319468
legacy.push(dir);
320-
errors.push(`Legacy skill alias directory: ${path.join(path.relative(resolved.projectRoot, resolved.skillsDir), dir)}`);
469+
legacyAliases.push(classification);
470+
if (classification.fatal) {
471+
errors.push(classification.message);
472+
} else {
473+
warnings.push(classification.message);
474+
}
321475
continue;
322476
}
323477
if (dir.startsWith('aiox-') && !expectedIds.has(dir)) {
@@ -332,6 +486,20 @@ function validateCodexSkills(options = {}) {
332486
content = '';
333487
}
334488

489+
const canonicalAgentPath = extractCanonicalAgentPath(content);
490+
const duplicateOf = canonicalAgentPath ? expectedByCanonicalPath.get(canonicalAgentPath) : null;
491+
if (duplicateOf && hasFullActivationPayload(content)) {
492+
duplicatePayloads.push({
493+
dir,
494+
canonicalSkillId: duplicateOf.skillId,
495+
canonicalAgentPath,
496+
});
497+
errors.push(
498+
`Duplicate full skill payload: ${path.join(path.relative(resolved.projectRoot, resolved.skillsDir), dir, 'SKILL.md')} -> ${duplicateOf.skillId} (${canonicalAgentPath})`,
499+
);
500+
continue;
501+
}
502+
335503
if (isGeneratedSquadSkill(content, resolved.projectRoot)) {
336504
ignored.push(dir);
337505
continue;
@@ -370,6 +538,8 @@ function validateCodexSkills(options = {}) {
370538
missing,
371539
orphaned,
372540
legacy,
541+
legacyAliases,
542+
duplicatePayloads,
373543
ignored,
374544
selfTests,
375545
};
@@ -424,6 +594,11 @@ module.exports = {
424594
runSkillSelfTests,
425595
extractGeneratedSquadSource,
426596
isGeneratedSquadSkill,
597+
hasFullActivationPayload,
598+
hasLegacyAliasIntent,
599+
getThinLegacyRedirectIssues,
600+
isIntentionalLegacyAlias,
601+
classifyLegacySkillAlias,
427602
parseArgs,
428603
getDefaultOptions,
429604
};

.aiox-core/install-manifest.yaml

Lines changed: 5 additions & 5 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-09T09:07:47.281Z"
11+
generated_at: "2026-05-10T01:14:38.554Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1128
1414
files:
@@ -3093,17 +3093,17 @@ files:
30933093
type: script
30943094
size: 22355
30953095
- path: infrastructure/scripts/codex-skills-sync/index.js
3096-
hash: sha256:caf8e4f6fad8d5b5c123bf47561a632bdb2cfacb5f3a80265de81ba0dea3f6b9
3096+
hash: sha256:2dc8637ba0095921e3cb5e99791dc05da61a12e375407b05f199696bcce7ea61
30973097
type: script
3098-
size: 5533
3098+
size: 5642
30993099
- path: infrastructure/scripts/codex-skills-sync/README.md
31003100
hash: sha256:2e4366ccc31b617a9be8a8b19c0379e1fd3ae64c4d5cfbb4d233860f9db0f949
31013101
type: script
31023102
size: 4072
31033103
- path: infrastructure/scripts/codex-skills-sync/validate.js
3104-
hash: sha256:5b8cb91ddcfda2b6b66328c12129e71665e4e1cb6f13b1792746e9d761d1ebcf
3104+
hash: sha256:6c8f98f49e433fc3aa648034457d5273a2ca667b8c7e492157a89ed6d0582c96
31053105
type: script
3106-
size: 12185
3106+
size: 17947
31073107
- path: infrastructure/scripts/collect-tool-usage.js
31083108
hash: sha256:8a739b79182dc41e28b7e02aeb9ec1dde5ec49f3ca534399acc59711b3b92bbf
31093109
type: script

.codex/skills/aiox-master/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ Use when you need comprehensive expertise across all domains, framework componen
1919
- `*kb` - Toggle KB mode (loads AIOX Method knowledge)
2020
- `*status` - Show current context and progress
2121
- `*guide` - Show comprehensive usage guide for this agent
22-
- `*exit` - Exit agent mode
2322
- `*create` - Create new AIOX component (agent, task, workflow, template, checklist)
2423
- `*modify` - Modify existing AIOX component
25-
- `*update-manifest` - Update team manifest
24+
- `*task` - Execute specific task (or list available)
25+
- `*workflow` - Start workflow (guided=manual, engine=real subagent spawning)
2626

2727
## Non-Negotiables
2828
- Follow `.aiox-core/constitution.md`.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# PRO-14.5 Legacy Slash-Command Shim Retirement Gate
2+
3+
Story: `STORY-PRO-14.5`
4+
Date: 2026-05-09
5+
Repo: `aiox-core`
6+
Status: implementation gate
7+
8+
## Purpose
9+
10+
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.
11+
12+
## Inventory
13+
14+
| Surface | Current classification | Evidence | Retirement posture |
15+
|---|---|---|---|
16+
| `.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. |
17+
| `.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. |
18+
| `.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`. |
19+
| `.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. |
20+
| `.claude/skills/AIOX/agents/*/SKILL.md` | canonical Claude skill payloads | 12 skill dirs observed. | Canonical Claude activation payloads. |
21+
| `.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. |
22+
| `.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. |
23+
| `.kimi/skills/` | generated projection | Observed 12 `aiox-*` skill dirs and 0 `aios-*` dirs. | Canonical projection. |
24+
| `.aiox-core/infrastructure/scripts/ide-sync/gemini-commands.js` | canonical Gemini command generator | Generates `aiox-{slug}.toml` and `aiox-menu.toml`. | Keep. |
25+
| `.gemini/commands/` | generated projection | Observed 13 `aiox-*` command files, including `aiox-menu.toml`, and 0 `aios-*` files. | Canonical projection. |
26+
| `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. |
27+
28+
## Blocking Rules
29+
30+
Strict Codex skill validation must fail when any of these conditions appears:
31+
32+
- a legacy `aios-*` directory contains a full activation payload instead of a classified redirect;
33+
- an orphaned canonical `aiox-*` directory duplicates the full payload of a known source agent;
34+
- an unexpected `aiox-*` generated skill directory is orphaned from source agents, unless it is an explicitly detected generated squad skill.
35+
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.
37+
38+
## Removal Criteria
39+
40+
Legacy slash-command shims or `aios-*` alias dirs may only be removed after all criteria below are true:
41+
42+
1. `npm run sync:skills:codex` produces no unexpected generated diff.
43+
2. `npm run validate:codex-skills:self-test` passes.
44+
3. `npm run validate:codex-sync` and `npm run validate:codex-integration` pass.
45+
4. `npm run sync:ide:check` passes for enabled IDE targets.
46+
5. `npm run validate:gemini-sync` and `npm run validate:gemini-integration` pass.
47+
6. The generated-surface inventory shows one canonical payload per supported agent per IDE surface.
48+
7. Legacy shim usage is either measured as zero for one full minor release or covered by a documented support cutoff.
49+
8. `@architect`, `@qa` and `@po` approve the removal story before deletion.
50+
51+
## Compatibility Window
52+
53+
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.
54+
55+
## Validation Evidence
56+
57+
Validated on 2026-05-09 from branch `feat/pro-14-5-legacy-shim-retirement`:
58+
59+
| Command | Result | Notes |
60+
|---|---|---|
61+
| `npm ci` | Pass | Installed dependencies cleanly; transient executable mode drift from install was reverted before commit. |
62+
| `npm run sync:skills:codex` | Pass | Generated 12 Codex skills. |
63+
| `npm run validate:codex-skills:self-test` | Pass | 12 skills checked; 12 self-tests passed. |
64+
| `npm run validate:codex-sync` | Pass | expected 12, synced 12, missing 0, drift 0, orphaned 0. |
65+
| `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. |
66+
| `npm run sync:ide:check` | Pass | expected 109, synced 109, missing 0, drift 0, orphaned 0. |
67+
| `npm run validate:gemini-sync` | Pass | expected 25, synced 25, missing 0, drift 0, orphaned 0. |
68+
| `npm run validate:gemini-integration` | Pass with warning | Warns `.gemini/rules.md` is not present; existing integration warning. |
69+
| `npm run lint` | Pass | Re-run after full tests released temporary fixture state. |
70+
| `npm run typecheck` | Pass | No TypeScript errors. |
71+
| `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. |
72+
| `npm run build` | Unavailable | Package has no `build` script; `npm run validate:publish` was run as the package publication gate instead. |
73+
| `npm run validate:publish` | Pass | Package contents, dependency completeness and publish safety gate passed. |
74+
| `git diff --check` | Pass | No whitespace errors. |
75+
| `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. |
76+
77+
## Rollback
78+
79+
If removal later breaks activation, rollback is:
80+
81+
1. Restore `.claude/commands/AIOX/agents/*.md` from the previous generator output or git history.
82+
2. Restore any removed legacy `aios-*` alias dirs as thin redirects, not full duplicated payloads.
83+
3. Run `npm run sync:skills:codex`.
84+
4. Run `npm run sync:ide`.
85+
5. Run all validation commands listed in the removal story.
86+
6. Publish a patch release only after validation passes.

0 commit comments

Comments
 (0)