feat: add skills system for extensible capability packages

Implement a three-tier skill system (bundled/managed/workspace) that
extends Flynn's abilities via SKILL.md instructions injected into the
system prompt.

- SkillManifest/Skill types with requirements gating (OS, binaries, env)
- Loader: discovers skills from directories, validates manifests,
  checks system requirements, infers manifest from SKILL.md if missing
- SkillRegistry: holds skills, generates system prompt additions,
  supports override by name (workspace > managed > bundled)
- SkillInstaller: copies/removes skills in managed directory with
  upgrade support
- Config: add skills.workspace_dir, managed_dir, bundled_dir options
- Daemon: loads all skills at startup, injects available skill
  instructions into the system prompt
- Tests: 45 new tests (loader 22, registry 11, installer 12)
This commit is contained in:
William Valentin
2026-02-05 20:20:03 -08:00
parent cd839c7f0c
commit 7c41ffad71
10 changed files with 1221 additions and 2 deletions
+66
View File
@@ -0,0 +1,66 @@
import type { Skill } from './types.js';
/**
* SkillRegistry holds loaded skills and generates system prompt additions.
*
* Skills are keyed by name. Managed/workspace skills may override bundled
* skills by registering with the same name.
*/
export class SkillRegistry {
private skills: Map<string, Skill> = new Map();
/** Register a skill. Replaces any existing skill with the same name. */
register(skill: Skill): void {
this.skills.set(skill.manifest.name, skill);
console.log(
`Skill '${skill.manifest.name}' registered (${skill.manifest.tier}, ${skill.available ? 'available' : 'unavailable'})`
);
}
/** Unregister a skill by name. Returns true if the skill existed. */
unregister(name: string): boolean {
return this.skills.delete(name);
}
/** Look up a skill by name. */
get(name: string): Skill | undefined {
return this.skills.get(name);
}
/** Return all registered skills. */
list(): Skill[] {
return Array.from(this.skills.values());
}
/** Return only skills whose requirements are met (available === true). */
listAvailable(): Skill[] {
return this.list().filter(skill => skill.available);
}
/**
* Generate system prompt additions from all available skills.
*
* Each skill's SKILL.md content is formatted as a markdown section:
* ```
* ## Skill: <name>
* <instructions>
* ```
*
* Returns an empty string if no skills are available.
*/
getSystemPromptAdditions(): string {
const available = this.listAvailable();
if (available.length === 0) {
return '';
}
return available
.map(skill => `## Skill: ${skill.manifest.name}\n${skill.instructions}`)
.join('\n\n');
}
/** Return the names of all available skills. */
getSkillNames(): string[] {
return this.listAvailable().map(skill => skill.manifest.name);
}
}