feat(skills): add refresh summary for discovery health
This commit is contained in:
+11
-2
@@ -1246,6 +1246,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"
|
||||||
|
},
|
||||||
|
"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": {
|
"overall_progress": {
|
||||||
"total_test_count": 1500,
|
"total_test_count": 1502,
|
||||||
"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%)",
|
||||||
@@ -1306,7 +1315,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 `flynn skills refresh` command dispatch"
|
"next_up": "Skills infrastructure Phase 1: add doctor enhancement for skills diagnostics"
|
||||||
},
|
},
|
||||||
"soul_md_and_cron_create": {
|
"soul_md_and_cron_create": {
|
||||||
"date": "2026-02-11",
|
"date": "2026-02-11",
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
toSkillListRows,
|
toSkillListRows,
|
||||||
renderSkillsTable,
|
renderSkillsTable,
|
||||||
renderSkillInfo,
|
renderSkillInfo,
|
||||||
|
summarizeSkillsRefresh,
|
||||||
|
renderSkillsRefreshSummary,
|
||||||
installSkillFromDirectory,
|
installSkillFromDirectory,
|
||||||
uninstallSkillByName,
|
uninstallSkillByName,
|
||||||
} from './skills.js';
|
} from './skills.js';
|
||||||
@@ -102,6 +104,31 @@ describe('skills CLI helpers', () => {
|
|||||||
expect(output).toContain('Unavailable reasons: Required binary not found');
|
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', () => {
|
it('installs a local skill directory', () => {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
const root = mkdtempSync(join(tmpdir(), 'flynn-skills-cli-'));
|
||||||
const sourceDir = join(root, 'source-skill');
|
const sourceDir = join(root, 'source-skill');
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ export interface SkillListRow {
|
|||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SkillRefreshSummary {
|
||||||
|
total: number;
|
||||||
|
available: number;
|
||||||
|
unavailable: number;
|
||||||
|
tiers: Record<Skill['manifest']['tier'], number>;
|
||||||
|
}
|
||||||
|
|
||||||
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
export function toSkillListRows(skills: Skill[]): SkillListRow[] {
|
||||||
return skills
|
return skills
|
||||||
.map((skill) => ({
|
.map((skill) => ({
|
||||||
@@ -71,6 +78,37 @@ export function renderSkillInfo(skill: Skill): string {
|
|||||||
return lines.join('\n');
|
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 } {
|
function loadSkillsFromConfig(configPath?: string): { skills?: Skill[]; error?: string } {
|
||||||
const loaded = loadConfigSafe(configPath);
|
const loaded = loadConfigSafe(configPath);
|
||||||
if (loaded.error || !loaded.config) {
|
if (loaded.error || !loaded.config) {
|
||||||
@@ -242,4 +280,26 @@ export function registerSkillsCommand(program: Command): void {
|
|||||||
|
|
||||||
console.log(`Uninstalled skill '${name}'.`);
|
console.log(`Uninstalled skill '${name}'.`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
skills
|
||||||
|
.command('refresh')
|
||||||
|
.description('Refresh skill discovery and print summary')
|
||||||
|
.option('--json', 'Output as JSON')
|
||||||
|
.option('-c, --config <path>', '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));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user