Skip to content

Commit 9d2f0b2

Browse files
sweinalvinunreal
andauthored
feat: add SKILLS.md loading system (#193)
* feat: add SKILLS.md loading system using similar path to KB system, skill.md parsing, L1 register, auto matching with TF-IDF scoring option, char-based context budget, tests and race-clean * fix: fixes for greptile code review for skills system * fix: update config.example.yaml with new skills system options * fix: honor skills config limits * chore: format skill registry * fix: improve skill validation reporting --------- Co-authored-by: Alvin Unreal <alvin@cmngoal.com>
1 parent 9e0f5d0 commit 9d2f0b2

11 files changed

Lines changed: 2005 additions & 8 deletions

README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@
5353
- [Creating Knowledge Bases](#creating-knowledge-bases)
5454
- [Using Knowledge Bases](#using-knowledge-bases)
5555
- [Auto-Loading Knowledge Bases](#auto-loading-knowledge-bases)
56+
- [Skills](#skills)
57+
- [Enabling Skills](#enabling-skills)
58+
- [Creating Skills](#creating-skills)
59+
- [Using Skills](#using-skills)
60+
- [Auto-Match](#auto-match)
61+
- [Budget Controls](#budget-controls)
5662
- [Model Configuration](#model-configuration)
5763
- [Setting Up Multiple Models](#setting-up-multiple-models)
5864
- [Switching Between Models](#switching-between-models)
@@ -428,6 +434,143 @@ knowledge_base:
428434
- Knowledge bases are injected after the system prompt but before conversation history
429435
- Unloading a KB removes it from future messages immediately
430436
437+
## Skills
438+
439+
The Skills feature extends the Knowledge Base system with structured, metadata-rich instructions that teach TmuxAI new capabilities. Unlike KBs (which provide passive reference material), skills can be auto-discovered, lazily loaded, and optionally auto-matched to incoming messages.
440+
441+
Each skill lives in a directory with a `SKILL.md` file containing frontmatter metadata and body content. Ancillary files (scripts, templates, reference docs) can coexist in the same directory.
442+
443+
### Enabling Skills
444+
445+
Skills are **disabled by default**. Enable them in `~/.config/tmuxai/config.yaml`:
446+
```yaml
447+
knowledge_base:
448+
skills:
449+
enabled: true
450+
```
451+
452+
### Creating Skills
453+
454+
Skills are stored in `~/.config/tmuxai/skills/<skill-name>/`:
455+
456+
```bash
457+
mkdir -p ~/.config/tmuxai/skills/git-hooks
458+
```
459+
460+
Create `SKILL.md` with frontmatter:
461+
```bash
462+
cat > ~/.config/tmuxai/skills/git-hooks/SKILL.md << 'EOF'
463+
---
464+
name: git-hooks
465+
description: Git pre-commit and linting setup. Auto-stage hooks, conventional commits, branch protection.
466+
disable-model-invocation: false
467+
---
468+
469+
# Git Hooks Guide
470+
471+
## Pre-commit Setup
472+
473+
```bash
474+
git config core.hooksPath .husky
475+
npm install husky --save-dev
476+
```
477+
478+
...rest of the skill body...
479+
EOF
480+
```
481+
482+
**Frontmatter fields:**
483+
484+
| Field | Required | Description |
485+
|-------|----------|-------------|
486+
| `name` | Yes | Unique skill name (must match directory name, alphanumeric + hyphens only) |
487+
| `description` | Yes | Brief description shown in L1 discovery block and `/skill list` |
488+
| `disable-model-invocation` | No | If `true`, disables auto-match — skill must be loaded manually |
489+
490+
Optional ancillary files (`.sh`, `.txt`, `.py`, `.json`) can be placed alongside `SKILL.md`. When a skill is loaded, TmuxAI includes a manifest listing those helper file paths so the model can request them if needed.
491+
492+
### Using Skills
493+
494+
```bash
495+
# List available skills
496+
TmuxAI » /skill
497+
Available skills:
498+
[ ] docker-workflows
499+
[ ] git-hooks [manual]
500+
[ ] terraform-best-practices
501+
502+
# Load a skill (lazy-load body + ancillary file manifest)
503+
TmuxAI » /skill load git-hooks
504+
✓ Loaded skill: git-hooks (1,240 chars)
505+
506+
# List again to see loaded status
507+
TmuxAI » /skill
508+
Available skills:
509+
[✓] docker-workflows (850 chars)
510+
[✓] git-hooks (1,240 chars)
511+
[ ] terraform-best-practices
512+
513+
Loaded: 2/3 skill(s), 2,090/32,000 chars
514+
515+
# View skill details without loading body
516+
TmuxAI » /skill info git-hooks
517+
Name: git-hooks
518+
Description: Git pre-commit and linting setup.
519+
Disabled: false
520+
Loaded: true
521+
Body Size: 1,240 chars
522+
Directory: ~/.config/tmuxai/skills/git-hooks
523+
File: ~/.config/tmuxai/skills/git-hooks/SKILL.md
524+
525+
# Validate all skills
526+
TmuxAI » /skill validate
527+
Validated 3 skill(s):
528+
✓ OK docker-workflows
529+
✓ OK git-hooks
530+
✓ OK terraform-best-practices
531+
532+
# Unload a skill
533+
TmuxAI » /skill unload git-hooks
534+
✓ Unloaded skill: git-hooks
535+
536+
# Unload all skills
537+
TmuxAI » /skill unload --all
538+
✓ Unloaded all skills (2 skill(s))
539+
```
540+
541+
### Auto-Match
542+
543+
You can enable automatic skill matching against incoming messages:
544+
545+
```yaml
546+
knowledge_base:
547+
skills:
548+
enabled: true
549+
auto_match: true
550+
auto_match_threshold: 0.1 # Match sensitivity (0.0–1.0, lower = more aggressive)
551+
```
552+
553+
With auto-match enabled, TmuxAI analyzes incoming messages and loads relevant skills based on term frequency and description relevance. Skills marked `[manual]` (via `disable-model-invocation: true`) require explicit loading.
554+
555+
### Budget Controls
556+
557+
Skills share context budget with your conversation. Defaults:
558+
559+
| Setting | Default | Description |
560+
|---------|---------|-------------|
561+
| `max_l1_chars` | 8,000 | Maximum chars for the L1 discovery block |
562+
| `max_loaded_chars` | 32,000 | Maximum chars across all loaded skill bodies |
563+
| `max_skill_chars` | 20,000 | Maximum chars per individual skill body; set to `0` to disable the per-skill cap |
564+
565+
Use `/info` to monitor context usage with skills loaded.
566+
567+
**Important Notes:**
568+
- Skills are injected after the system prompt and KBs, before conversation history
569+
- The L1 discovery block tells the model which skills exist and their load status
570+
- Body content is only loaded on demand (lazy loading)
571+
- A 1MB cap per SKILL.md prevents runaway memory usage
572+
- SKILL.md frontmatter fences (`---`) are matched line-by-line; standalone `---` lines in multi-line YAML values will be misinterpreted as the closing fence
573+
431574
## Model Configuration
432575

433576
TmuxAI supports configuring multiple AI model configurations and easily switching between them. This allows you to define different AI providers, models, and settings for various use cases.

config.example.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,18 @@ blacklist_patterns:
115115
- 'mv\s+'
116116
- 'dd\s+'
117117

118+
# Knowledge Base: Skills system (opt-in)
119+
knowledge_base:
120+
skills:
121+
enabled: false # Disabled by default; set true to enable
122+
auto_scan: true
123+
auto_match: false
124+
auto_match_threshold: 0.1 # Match sensitivity (0.0–1.0, lower = more aggressive)
125+
max_l1_chars: 8000 # L1 discovery block char budget
126+
max_loaded_chars: 32000 # Total loaded skill bodies char budget
127+
max_skill_chars: 20000 # Per-skill char cap (set 0 for unlimited; 1MB hard limit)
128+
truncate_desc_at: 200 # Truncate descriptions in L1 block
129+
118130
# Prompts customization, see prompts.go for more details
119131
prompts:
120132
base_system: |

config/config.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,23 @@ type PromptsConfig struct {
9090
Watch string `mapstructure:"watch"`
9191
}
9292

93+
// SkillsConfig holds skill system configuration
94+
type SkillsConfig struct {
95+
Enabled bool `mapstructure:"enabled"`
96+
AutoScan bool `mapstructure:"auto_scan"`
97+
AutoMatch bool `mapstructure:"auto_match"`
98+
AutoMatchThreshold float64 `mapstructure:"auto_match_threshold"`
99+
MaxL1Chars int `mapstructure:"max_l1_chars"`
100+
MaxLoadedChars int `mapstructure:"max_loaded_chars"`
101+
MaxSkillChars int `mapstructure:"max_skill_chars"`
102+
TruncateDescAt int `mapstructure:"truncate_desc_at"`
103+
}
104+
93105
// KnowledgeBaseConfig holds knowledge base configuration
94106
type KnowledgeBaseConfig struct {
95-
AutoLoad []string `mapstructure:"auto_load"`
96-
Path string `mapstructure:"path"`
107+
AutoLoad []string `mapstructure:"auto_load"`
108+
Path string `mapstructure:"path"`
109+
Skills SkillsConfig `mapstructure:"skills"`
97110
}
98111

99112
// TmuxConfig holds tmux-specific behavior settings.
@@ -135,6 +148,16 @@ func DefaultConfig() *Config {
135148
KnowledgeBase: KnowledgeBaseConfig{
136149
AutoLoad: []string{},
137150
Path: "",
151+
Skills: SkillsConfig{
152+
Enabled: false,
153+
AutoScan: true,
154+
AutoMatch: false,
155+
AutoMatchThreshold: 0.1,
156+
MaxL1Chars: 8000,
157+
MaxLoadedChars: 32000,
158+
MaxSkillChars: 20000,
159+
TruncateDescAt: 200,
160+
},
138161
},
139162
}
140163
}

internal/chat.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,44 @@ func (c *CLIInterface) newCompleter() *completion.CmdCompletionOrList2 {
288288
return availableModels, availableModels
289289
}
290290
}
291+
292+
// Handle /skill subcommands
293+
if len(field) > 0 && field[0] == "/skill" {
294+
if len(field) == 1 || (len(field) == 2 && !strings.HasSuffix(field[1], " ")) {
295+
return []string{"list ", "load ", "unload ", "info ", "validate "}, []string{"list ", "load ", "unload ", "info ", "validate "}
296+
} else if len(field) >= 2 && field[1] == "load" {
297+
// Complete with skill names
298+
if c.manager.Skills != nil {
299+
var skillNames []string
300+
for name := range c.manager.Skills.Skills {
301+
skillNames = append(skillNames, name)
302+
}
303+
return skillNames, skillNames
304+
}
305+
} else if len(field) >= 2 && field[1] == "info" {
306+
// Complete with skill names
307+
if c.manager.Skills != nil {
308+
var skillNames []string
309+
for name := range c.manager.Skills.Skills {
310+
skillNames = append(skillNames, name)
311+
}
312+
return skillNames, skillNames
313+
}
314+
} else if len(field) >= 2 && field[1] == "unload" {
315+
// Complete with loaded skill names and --all
316+
var unloadTargets []string
317+
unloadTargets = append(unloadTargets, "--all ")
318+
if c.manager.Skills != nil {
319+
for name, skill := range c.manager.Skills.Skills {
320+
if skill.Loaded {
321+
unloadTargets = append(unloadTargets, name+" ")
322+
}
323+
}
324+
}
325+
return unloadTargets, unloadTargets
326+
}
327+
}
328+
291329
return nil, nil
292330
},
293331
}

0 commit comments

Comments
 (0)