feat(doctor): surface skill directory health in diagnostics
This commit is contained in:
+12
-3
@@ -1203,7 +1203,7 @@
|
|||||||
"phases": {
|
"phases": {
|
||||||
"phase_1_command_dispatch": {
|
"phase_1_command_dispatch": {
|
||||||
"priority": "P0",
|
"priority": "P0",
|
||||||
"status": "in_progress",
|
"status": "completed",
|
||||||
"description": "flynn skills CLI commands (list/info/install/uninstall/refresh) with doctor enhancement",
|
"description": "flynn skills CLI commands (list/info/install/uninstall/refresh) with doctor enhancement",
|
||||||
"effort": "2-3 hours",
|
"effort": "2-3 hours",
|
||||||
"sub_slices": {
|
"sub_slices": {
|
||||||
@@ -1255,6 +1255,15 @@
|
|||||||
"src/cli/skills.test.ts"
|
"src/cli/skills.test.ts"
|
||||||
],
|
],
|
||||||
"test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing"
|
"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": {
|
"overall_progress": {
|
||||||
"total_test_count": 1502,
|
"total_test_count": 1504,
|
||||||
"all_tests_passing": true,
|
"all_tests_passing": true,
|
||||||
"p0_completion": "3/3 (100%)",
|
"p0_completion": "3/3 (100%)",
|
||||||
"p1_completion": "4/4 (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",
|
"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",
|
"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",
|
"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": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
@@ -173,4 +173,54 @@ automation:
|
|||||||
const telegramCheck = results.find(r => r.label.includes('Telegram'));
|
const telegramCheck = results.find(r => r.label.includes('Telegram'));
|
||||||
expect(telegramCheck?.status).toBe('skip');
|
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)' };
|
return { status: 'skip', label: 'Skills loaded', detail: '(config invalid)' };
|
||||||
}
|
}
|
||||||
try {
|
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 { loadAllSkills } = await import('../skills/index.js');
|
||||||
const skills = loadAllSkills({
|
const skills = loadAllSkills({
|
||||||
bundledDir: ctx.config.skills.bundled_dir,
|
bundledDir: skillDirs.bundled,
|
||||||
managedDir: ctx.config.skills.managed_dir,
|
managedDir: skillDirs.managed,
|
||||||
workspaceDir: ctx.config.skills.workspace_dir,
|
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) {
|
} catch (err) {
|
||||||
return { status: 'fail', label: 'Skills loaded', detail: err instanceof Error ? err.message : String(err) };
|
return { status: 'fail', label: 'Skills loaded', detail: err instanceof Error ? err.message : String(err) };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user