feat(doctor): surface skill directory health in diagnostics
This commit is contained in:
+12
-3
@@ -1203,7 +1203,7 @@
|
||||
"phases": {
|
||||
"phase_1_command_dispatch": {
|
||||
"priority": "P0",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"description": "flynn skills CLI commands (list/info/install/uninstall/refresh) with doctor enhancement",
|
||||
"effort": "2-3 hours",
|
||||
"sub_slices": {
|
||||
@@ -1255,6 +1255,15 @@
|
||||
"src/cli/skills.test.ts"
|
||||
],
|
||||
"test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
||||
},
|
||||
"doctor_skills_diagnostics": {
|
||||
"status": "completed",
|
||||
"description": "Enhanced `flynn doctor` skills diagnostics to report availability counts and warn on missing configured skill directories",
|
||||
"files_modified": [
|
||||
"src/cli/doctor.ts",
|
||||
"src/cli/doctor.test.ts"
|
||||
],
|
||||
"test_status": "typecheck + targeted doctor tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1295,7 +1304,7 @@
|
||||
},
|
||||
|
||||
"overall_progress": {
|
||||
"total_test_count": 1502,
|
||||
"total_test_count": 1504,
|
||||
"all_tests_passing": true,
|
||||
"p0_completion": "3/3 (100%)",
|
||||
"p1_completion": "4/4 (100%)",
|
||||
@@ -1315,7 +1324,7 @@
|
||||
"gmail_auth_cli": "flynn gmail-auth command implemented with OAuth2 flow, doctor check, config routed to Telegram",
|
||||
"native_audio_support": "completed — smart routing for native audio (Gemini/OpenAI/GitHub) vs Whisper transcription fallback",
|
||||
"remaining_phases_completion": "Phase 1: 3/3 (100%) — context levels, command registry, memory structure. Phase 2: 2/2 (100%) — component registry, confidence routing. Phase 3: 2/2 (100%) — adaptive memory/compaction, truthfulness/autonomy hardening",
|
||||
"next_up": "Skills infrastructure Phase 1: add doctor enhancement for skills diagnostics"
|
||||
"next_up": "Skills infrastructure Phase 2: implement skill auto-reload watcher with configurable debounce"
|
||||
},
|
||||
"soul_md_and_cron_create": {
|
||||
"date": "2026-02-11",
|
||||
|
||||
@@ -173,4 +173,54 @@ automation:
|
||||
const telegramCheck = results.find(r => r.label.includes('Telegram'));
|
||||
expect(telegramCheck?.status).toBe('skip');
|
||||
});
|
||||
|
||||
it('reports WARN for skills when configured directories are missing', async () => {
|
||||
mkdirSync(testDir, { recursive: true });
|
||||
const configPath = join(testDir, 'skills-missing.yaml');
|
||||
writeFileSync(configPath, `
|
||||
telegram:
|
||||
bot_token: "test-token"
|
||||
allowed_chat_ids: [123]
|
||||
models:
|
||||
default:
|
||||
provider: anthropic
|
||||
model: claude-sonnet
|
||||
skills:
|
||||
bundled_dir: "${join(testDir, 'missing-bundled')}"
|
||||
`);
|
||||
|
||||
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
||||
const results = await runChecks(ctx);
|
||||
|
||||
const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
|
||||
expect(skillsCheck?.status).toBe('warn');
|
||||
expect(skillsCheck?.detail).toContain('missing dirs');
|
||||
});
|
||||
|
||||
it('reports PASS for skills when configured directory exists', async () => {
|
||||
const bundledDir = join(testDir, 'bundled');
|
||||
const sampleSkillDir = join(bundledDir, 'sample');
|
||||
mkdirSync(sampleSkillDir, { recursive: true });
|
||||
writeFileSync(join(sampleSkillDir, 'SKILL.md'), '# Sample skill');
|
||||
|
||||
const configPath = join(testDir, 'skills-pass.yaml');
|
||||
writeFileSync(configPath, `
|
||||
telegram:
|
||||
bot_token: "test-token"
|
||||
allowed_chat_ids: [123]
|
||||
models:
|
||||
default:
|
||||
provider: anthropic
|
||||
model: claude-sonnet
|
||||
skills:
|
||||
bundled_dir: "${bundledDir}"
|
||||
`);
|
||||
|
||||
const ctx: DoctorContext = { configPath, dataDir: testDir };
|
||||
const results = await runChecks(ctx);
|
||||
|
||||
const skillsCheck = results.find(r => r.label.includes('Skills loaded'));
|
||||
expect(skillsCheck?.status).toBe('pass');
|
||||
expect(skillsCheck?.detail).toContain('1 skill(s), 1 available, 0 unavailable');
|
||||
});
|
||||
});
|
||||
|
||||
+22
-4
@@ -189,13 +189,31 @@ const checkSkills: Check = async (ctx) => {
|
||||
return { status: 'skip', label: 'Skills loaded', detail: '(config invalid)' };
|
||||
}
|
||||
try {
|
||||
const skillDirs = {
|
||||
bundled: ctx.config.skills.bundled_dir,
|
||||
managed: ctx.config.skills.managed_dir,
|
||||
workspace: ctx.config.skills.workspace_dir,
|
||||
};
|
||||
const missingDirs = Object.entries(skillDirs)
|
||||
.filter(([, dir]) => Boolean(dir) && !existsSync(dir as string))
|
||||
.map(([tier, dir]) => `${tier}:${dir as string}`);
|
||||
|
||||
const { loadAllSkills } = await import('../skills/index.js');
|
||||
const skills = loadAllSkills({
|
||||
bundledDir: ctx.config.skills.bundled_dir,
|
||||
managedDir: ctx.config.skills.managed_dir,
|
||||
workspaceDir: ctx.config.skills.workspace_dir,
|
||||
bundledDir: skillDirs.bundled,
|
||||
managedDir: skillDirs.managed,
|
||||
workspaceDir: skillDirs.workspace,
|
||||
});
|
||||
return { status: 'pass', label: 'Skills loaded', detail: `(${skills.length} skill(s))` };
|
||||
|
||||
const available = skills.filter((skill) => skill.available).length;
|
||||
const unavailable = skills.length - available;
|
||||
const detailParts = [`${skills.length} skill(s), ${available} available, ${unavailable} unavailable`];
|
||||
if (missingDirs.length > 0) {
|
||||
detailParts.push(`missing dirs: ${missingDirs.join(', ')}`);
|
||||
return { status: 'warn', label: 'Skills loaded', detail: detailParts.join(' — ') };
|
||||
}
|
||||
|
||||
return { status: 'pass', label: 'Skills loaded', detail: detailParts.join(' — ') };
|
||||
} catch (err) {
|
||||
return { status: 'fail', label: 'Skills loaded', detail: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user