From fc3d2ab4d87f6b1804516414a34b1a639dd895b4 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Thu, 12 Feb 2026 17:02:23 -0800 Subject: [PATCH] feat(skills): add refresh summary for discovery health --- docs/plans/state.json | 13 +++++++-- src/cli/skills.test.ts | 27 +++++++++++++++++++ src/cli/skills.ts | 60 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/docs/plans/state.json b/docs/plans/state.json index 3e13e30..2bc45dd 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -1246,6 +1246,15 @@ "src/cli/skills.test.ts" ], "test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing" + }, + "skills_refresh_command": { + "status": "completed", + "description": "Added `flynn skills refresh` command dispatch with human-readable and JSON summary output for current skill discovery", + "files_modified": [ + "src/cli/skills.ts", + "src/cli/skills.test.ts" + ], + "test_status": "typecheck + targeted skills CLI tests + full test suite + lint (warnings only, 0 errors) + build passing" } } }, @@ -1286,7 +1295,7 @@ }, "overall_progress": { - "total_test_count": 1500, + "total_test_count": 1502, "all_tests_passing": true, "p0_completion": "3/3 (100%)", "p1_completion": "4/4 (100%)", @@ -1306,7 +1315,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 `flynn skills refresh` command dispatch" + "next_up": "Skills infrastructure Phase 1: add doctor enhancement for skills diagnostics" }, "soul_md_and_cron_create": { "date": "2026-02-11", diff --git a/src/cli/skills.test.ts b/src/cli/skills.test.ts index 625ee97..aefa385 100644 --- a/src/cli/skills.test.ts +++ b/src/cli/skills.test.ts @@ -7,6 +7,8 @@ import { toSkillListRows, renderSkillsTable, renderSkillInfo, + summarizeSkillsRefresh, + renderSkillsRefreshSummary, installSkillFromDirectory, uninstallSkillByName, } from './skills.js'; @@ -102,6 +104,31 @@ describe('skills CLI helpers', () => { expect(output).toContain('Unavailable reasons: Required binary not found'); }); + it('summarizes refresh counts across status and tiers', () => { + const summary = summarizeSkillsRefresh([ + buildSkill({ manifest: { name: 'a', description: 'a', version: '1.0.0', tier: 'bundled' } }), + buildSkill({ manifest: { name: 'b', description: 'b', version: '1.0.0', tier: 'managed' } }), + buildSkill({ available: false, manifest: { name: 'c', description: 'c', version: '1.0.0', tier: 'workspace' } }), + ]); + + expect(summary.total).toBe(3); + expect(summary.available).toBe(2); + expect(summary.unavailable).toBe(1); + expect(summary.tiers).toEqual({ bundled: 1, managed: 1, workspace: 1 }); + }); + + it('renders refresh summary text', () => { + const output = renderSkillsRefreshSummary({ + total: 4, + available: 3, + unavailable: 1, + tiers: { bundled: 2, managed: 1, workspace: 1 }, + }); + + expect(output).toContain('Refreshed 4 skills'); + expect(output).toContain('By tier: bundled=2, managed=1, workspace=1'); + }); + it('installs a local skill directory', () => { const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-')); const sourceDir = join(root, 'source-skill'); diff --git a/src/cli/skills.ts b/src/cli/skills.ts index d2d0cf3..2832943 100644 --- a/src/cli/skills.ts +++ b/src/cli/skills.ts @@ -12,6 +12,13 @@ export interface SkillListRow { reason?: string; } +export interface SkillRefreshSummary { + total: number; + available: number; + unavailable: number; + tiers: Record; +} + export function toSkillListRows(skills: Skill[]): SkillListRow[] { return skills .map((skill) => ({ @@ -71,6 +78,37 @@ export function renderSkillInfo(skill: Skill): string { return lines.join('\n'); } +export function summarizeSkillsRefresh(skills: Skill[]): SkillRefreshSummary { + const summary: SkillRefreshSummary = { + total: skills.length, + available: 0, + unavailable: 0, + tiers: { + bundled: 0, + managed: 0, + workspace: 0, + }, + }; + + for (const skill of skills) { + if (skill.available) { + summary.available += 1; + } else { + summary.unavailable += 1; + } + summary.tiers[skill.manifest.tier] += 1; + } + + return summary; +} + +export function renderSkillsRefreshSummary(summary: SkillRefreshSummary): string { + return [ + `Refreshed ${summary.total} skills (${summary.available} available, ${summary.unavailable} unavailable).`, + `By tier: bundled=${summary.tiers.bundled}, managed=${summary.tiers.managed}, workspace=${summary.tiers.workspace}`, + ].join('\n'); +} + function loadSkillsFromConfig(configPath?: string): { skills?: Skill[]; error?: string } { const loaded = loadConfigSafe(configPath); if (loaded.error || !loaded.config) { @@ -242,4 +280,26 @@ export function registerSkillsCommand(program: Command): void { console.log(`Uninstalled skill '${name}'.`); }); + + skills + .command('refresh') + .description('Refresh skill discovery and print summary') + .option('--json', 'Output as JSON') + .option('-c, --config ', 'Config file path') + .action((opts: { json?: boolean; config?: string }) => { + const loaded = loadSkillsFromConfig(opts.config); + if (loaded.error || !loaded.skills) { + console.error(loaded.error ?? 'Failed to load skills'); + process.exitCode = 1; + return; + } + + const summary = summarizeSkillsRefresh(loaded.skills); + if (opts.json) { + console.log(JSON.stringify(summary, null, 2)); + return; + } + + console.log(renderSkillsRefreshSummary(summary)); + }); }