Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .aiox-core/core-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ ideSync:
claude-code:
enabled: true
path: .claude/commands/AIOX/agents
skillsPath: .claude/skills
format: full-markdown-yaml
codex:
enabled: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"properties": {
"enabled": { "type": "boolean" },
"path": { "type": "string" },
"skillsPath": { "type": "string" },
"format": { "type": "string", "enum": ["full-markdown-yaml", "condensed-rules", "cursor-style"] }
},
"additionalProperties": false
Expand Down
106 changes: 81 additions & 25 deletions .aiox-core/core/doctor/checks/ide-sync.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Doctor Check: IDE Sync
*
* Validates agents in .claude/commands/AIOX/agents/ match
* .aiox-core/development/agents/ (count and names).
* Validates Claude agent skills and legacy command files match
* .aiox-core/development/agents/ during the skills-first transition.
*
* @module aiox-core/doctor/checks/ide-sync
* @story INS-4.1
Expand All @@ -13,9 +13,54 @@ const fs = require('fs');

const name = 'ide-sync';

function readMarkdownAgents(dir, label) {
if (!fs.existsSync(dir)) {
return { agents: [], error: null };
}

try {
const agents = fs.readdirSync(dir)
.filter((f) => f.endsWith('.md'))
.map((f) => f.replace('.md', ''));
return { agents, error: null };
} catch (error) {
return { agents: [], error: `Cannot read ${label} directory: ${error.message}` };
}
}

function readSkillAgents(dir) {
if (!fs.existsSync(dir)) {
return { agents: [], error: null };
}

try {
const agents = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(dir, entry.name, 'SKILL.md')))
.map((entry) => entry.name);
return { agents, error: null };
} catch (error) {
return { agents: [], error: `Cannot read Claude skills directory: ${error.message}` };
}
}

function diffAgents(expected, actual) {
const expectedSet = new Set(expected);
const actualSet = new Set(actual);
return {
missing: expected.filter((id) => !actualSet.has(id)),
extra: actual.filter((id) => !expectedSet.has(id)),
};
}

function formatList(items) {
if (items.length === 0) return 'none';
return items.slice(0, 5).join(', ') + (items.length > 5 ? `, +${items.length - 5} more` : '');
}

async function run(context) {
const agentsSourceDir = path.join(context.projectRoot, '.aiox-core', 'development', 'agents');
const agentsIdeDir = path.join(context.projectRoot, '.claude', 'commands', 'AIOX', 'agents');
const agentsCommandDir = path.join(context.projectRoot, '.claude', 'commands', 'AIOX', 'agents');
const agentsSkillDir = path.join(context.projectRoot, '.claude', 'skills', 'AIOX', 'agents');

if (!fs.existsSync(agentsSourceDir)) {
return {
Expand All @@ -26,16 +71,7 @@ async function run(context) {
};
}

if (!fs.existsSync(agentsIdeDir)) {
return {
check: name,
status: 'WARN',
message: 'IDE agents directory not found (.claude/commands/AIOX/agents/)',
fixCommand: 'npx aiox-core install --force',
};
}

let sourceAgents, ideFiles;
let sourceAgents;
try {
sourceAgents = fs.readdirSync(agentsSourceDir)
.filter((f) => f.endsWith('.md'))
Expand All @@ -49,37 +85,57 @@ async function run(context) {
};
}

try {
ideFiles = fs.readdirSync(agentsIdeDir)
.filter((f) => f.endsWith('.md'));
} catch (_err) {
const commandResult = readMarkdownAgents(agentsCommandDir, 'Claude commands');
const skillResult = readSkillAgents(agentsSkillDir);

if (commandResult.error || skillResult.error) {
return {
check: name,
status: 'WARN',
message: 'Cannot read IDE agents directory',
status: 'FAIL',
message: commandResult.error || skillResult.error,
fixCommand: 'npx aiox-core install --force',
};
}

const ideAgents = ideFiles.map((f) => f.replace('.md', ''));
const commandAgents = commandResult.agents;
const skillAgents = skillResult.agents;

const sourceCount = sourceAgents.length;
const ideCount = ideAgents.length;
const commandCount = commandAgents.length;
const skillCount = skillAgents.length;
const skillDiff = diffAgents(sourceAgents, skillAgents);
const commandDiff = diffAgents(sourceAgents, commandAgents);

if (skillDiff.missing.length > 0 || skillDiff.extra.length > 0) {
return {
check: name,
status: 'WARN',
message: `Claude skills mismatch (missing: ${formatList(skillDiff.missing)}; extra: ${formatList(skillDiff.extra)})`,
fixCommand: 'npx aiox-core install --force',
};
}

if (sourceCount === ideCount) {
if (commandDiff.missing.length === 0 && commandDiff.extra.length === 0) {
return {
check: name,
status: 'PASS',
message: `${ideCount}/${sourceCount} agents synced`,
message: `${skillCount}/${sourceCount} Claude skills synced; ${commandCount}/${sourceCount} legacy commands synced`,
fixCommand: null,
};
}

return {
check: name,
status: 'WARN',
message: `IDE has ${ideCount} agents, source has ${sourceCount}`,
message: `${skillCount}/${sourceCount} Claude skills synced; legacy commands mismatch (missing: ${formatList(commandDiff.missing)}; extra: ${formatList(commandDiff.extra)}; count: ${commandCount}/${sourceCount})`,
fixCommand: 'npx aiox-core install --force',
};
}

module.exports = { name, run };
module.exports = {
name,
run,
readMarkdownAgents,
readSkillAgents,
diffAgents,
};
98 changes: 83 additions & 15 deletions .aiox-core/core/doctor/checks/skills-count.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/**
* Doctor Check: Skills Count
*
* Counts skill directories in .claude/skills/ that contain SKILL.md.
* PASS: >=7, WARN: 1-6, FAIL: 0 or directory missing.
* Counts SKILL.md files in .claude/skills/ recursively.
* PASS: >=7 total skills and all AIOX agent skills present.
* WARN: skills exist but AIOX agent skills are incomplete.
* FAIL: 0 or directory missing.
*
* @module aiox-core/doctor/checks/skills-count
* @story INS-4.8
Expand All @@ -13,8 +15,61 @@ const fs = require('fs');

const name = 'skills-count';

function countSkillFiles(dir) {
let count = 0;
let entries;

try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch (error) {
if (error && error.code === 'ENOENT') return 0;
throw new Error(`Cannot read skills directory "${dir}": ${error.message}`);
}

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
count += countSkillFiles(fullPath);
} else if (entry.isFile() && entry.name === 'SKILL.md') {
count++;
}
}

return count;
}

function countAgentSkillFiles(agentSkillsDir) {
let entries;

try {
entries = fs.readdirSync(agentSkillsDir, { withFileTypes: true });
} catch (error) {
if (error && error.code === 'ENOENT') return 0;
throw new Error(`Cannot read AIOX agent skills directory "${agentSkillsDir}": ${error.message}`);
}

return entries.filter((entry) => (
entry.isDirectory() && fs.existsSync(path.join(agentSkillsDir, entry.name, 'SKILL.md'))
)).length;
}

function countSourceAgents(sourceAgentsDir) {
let entries;

try {
entries = fs.readdirSync(sourceAgentsDir, { withFileTypes: true });
} catch (error) {
if (error && error.code === 'ENOENT') return 0;
throw new Error(`Cannot read source agents directory "${sourceAgentsDir}": ${error.message}`);
}

return entries.filter((entry) => entry.isFile() && entry.name.endsWith('.md')).length;
}

async function run(context) {
const skillsDir = path.join(context.projectRoot, '.claude', 'skills');
const agentSkillsDir = path.join(skillsDir, 'AIOX', 'agents');
const sourceAgentsDir = path.join(context.projectRoot, '.aiox-core', 'development', 'agents');

if (!fs.existsSync(skillsDir)) {
return {
Expand All @@ -25,24 +80,22 @@ async function run(context) {
};
}

let entries;
let count;
let agentSkillCount;
let sourceAgentCount;
try {
entries = fs.readdirSync(skillsDir, { withFileTypes: true });
} catch {
count = countSkillFiles(skillsDir);
agentSkillCount = countAgentSkillFiles(agentSkillsDir);
sourceAgentCount = countSourceAgents(sourceAgentsDir);
} catch (error) {
return {
check: name,
status: 'FAIL',
message: 'Cannot read skills directory',
message: error.message,
fixCommand: 'npx aiox-core install --force',
};
}

const skills = entries.filter(
(d) => d.isDirectory() && fs.existsSync(path.join(skillsDir, d.name, 'SKILL.md')),
);

const count = skills.length;

if (count === 0) {
return {
check: name,
Expand All @@ -52,21 +105,36 @@ async function run(context) {
};
}

if (sourceAgentCount > 0 && agentSkillCount !== sourceAgentCount) {
return {
check: name,
status: 'WARN',
message: `${count} skills found, but AIOX agent skills are incomplete (${agentSkillCount}/${sourceAgentCount})`,
fixCommand: 'npx aiox-core install --force',
};
}

if (count >= 7) {
return {
check: name,
status: 'PASS',
message: `${count} skills found`,
message: `${count} skills found (${agentSkillCount}/${sourceAgentCount} AIOX agent skills)`,
fixCommand: null,
};
}

return {
check: name,
status: 'WARN',
message: `Only ${count}/7 skills found`,
message: `Only ${count}/7 skills found (${agentSkillCount}/${sourceAgentCount} AIOX agent skills)`,
fixCommand: 'npx aiox-core install --force',
};
}

module.exports = { name, run };
module.exports = {
name,
run,
countSkillFiles,
countAgentSkillFiles,
countSourceAgents,
};
Loading
Loading