From 0a19f01639f5924abba4ca4bafb1f9a7f202cfff Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 12 Feb 2026 17:05:04 -0800 Subject: [PATCH] feat(doctor): surface skill directory health in diagnostics --- docs/plans/state.json | 15 ++++++++++--- src/cli/doctor.test.ts | 50 ++++++++++++++++++++++++++++++++++++++++++ src/cli/doctor.ts | 26 ++++++++++++++++++---- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 2bc45dd..024e170 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -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", diff --git a/src/cli/doctor.test.ts b/src/cli/doctor.test.ts index aa9d197..40605b2 100644 --- a/src/cli/doctor.test.ts +++ b/src/cli/doctor.test.ts @@ -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'); + }); }); diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index f8764b4..6f14ed0 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -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) }; }