mirror of
https://github.com/patriceckhart/zot.git
synced 2026-07-03 08:39:52 +02:00
feat(skills): user skills now opt-in via --with-skills
Behaviour change: a fresh zot run now loads only the built-in skills compiled into the binary. User-installed SKILL.md files under $ZOT_HOME/skills/, .zot/skills/, .claude/skills/, or .agents/skills/ stay dormant until the user explicitly opts in with --with-skills. Three discrete modes: zot built-ins only (default) zot --with-skills built-ins + user skills zot --no-skill nothing (no skill tool, no manifest) Rationale: a fresh install should have a deterministic skill set, regardless of what's already lying around in $ZOT_HOME from old experiments. Built-ins ship with the binary so they're auditable; user skills are loaded only when the user explicitly asks for them. Discover() gained an includeUser bool (was 3 args, now 4). The in-tree caller updated; the test that exercised the old signature gets includeUser=true so its existing assertions still hold. scanUserSkills() split out so the includeUser=false path is a cheap no-op. End-to-end verified live: default -> write-zot-extension (only) --with-skills -> write-zot-extension + code-review + test-fix --no-skill -> no skill tool, no manifest at all
This commit is contained in:
parent
e9ffc74442
commit
fbad128c4c
5 changed files with 58 additions and 28 deletions
|
|
@ -56,6 +56,14 @@ type Args struct {
|
|||
// extra context biasing the model.
|
||||
NoSkill bool
|
||||
|
||||
// WithSkills opts into loading user-installed skills from
|
||||
// $ZOT_HOME/skills/, .zot/skills/, .claude/skills/, and
|
||||
// .agents/skills/. Without this flag only the built-in skills
|
||||
// shipped with the zot binary are available, so a fresh install
|
||||
// has a deterministic skill set regardless of what's lying
|
||||
// around in the user's home directory.
|
||||
WithSkills bool
|
||||
|
||||
ListModels bool
|
||||
Help bool
|
||||
Version bool
|
||||
|
|
@ -151,6 +159,8 @@ func ParseArgs(in []string) (Args, error) {
|
|||
a.NoExt = true
|
||||
case "--no-skill", "--no-skills":
|
||||
a.NoSkill = true
|
||||
case "--with-skills", "--with-skill":
|
||||
a.WithSkills = true
|
||||
case "--reasoning":
|
||||
v, err := want(&i, arg)
|
||||
if err != nil {
|
||||
|
|
@ -259,6 +269,10 @@ flags:
|
|||
--no-skill skip skill discovery for this run,
|
||||
including built-in skills (no skill
|
||||
tool, no Available skills manifest)
|
||||
--with-skills load user-installed skills from
|
||||
$ZOT_HOME/skills/ + .zot/skills/ +
|
||||
.claude/skills/ + .agents/skills/.
|
||||
default: only built-in skills load
|
||||
|
||||
--max-steps N agent loop iteration cap (default 50)
|
||||
--list-models print known models and exit
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
|
|||
)
|
||||
if !args.NoSkill {
|
||||
homeDir, _ := os.UserHomeDir()
|
||||
discovered, _ = skills.Discover(ZotHome(), args.CWD, homeDir)
|
||||
discovered, _ = skills.Discover(ZotHome(), args.CWD, homeDir, args.WithSkills)
|
||||
if len(discovered) > 0 {
|
||||
skillTool = skills.NewTool(discovered)
|
||||
reg[skillTool.Name()] = skillTool
|
||||
|
|
|
|||
|
|
@ -417,9 +417,11 @@ func runInteractive(ctx context.Context, args Args, version string) error {
|
|||
// out built-in skills — they're hidden from user-facing
|
||||
// surfaces because they're implementation detail; the
|
||||
// model still sees them through the system-prompt
|
||||
// manifest and the skill tool.
|
||||
// manifest and the skill tool. User skills only appear
|
||||
// when --with-skills is set; without it the picker shows
|
||||
// nothing but the model still has the built-ins.
|
||||
userHome, _ := os.UserHomeDir()
|
||||
list, _ := skills.Discover(ZotHome(), r.CWD, userHome)
|
||||
list, _ := skills.Discover(ZotHome(), r.CWD, userHome, args.WithSkills)
|
||||
return skills.VisibleSkills(list)
|
||||
},
|
||||
PersistModel: func(providerName, model string) {
|
||||
|
|
|
|||
|
|
@ -83,19 +83,46 @@ func VisibleSkills(in []*Skill) []*Skill {
|
|||
return out
|
||||
}
|
||||
|
||||
// Discover walks every supported location, parses each SKILL.md, and
|
||||
// returns the merged skill set. First-match-wins per name; the order
|
||||
// matches the priority list in the package doc. Errors per skill are
|
||||
// returned alongside the partial result so a single broken file
|
||||
// doesn't suppress the rest.
|
||||
// Discover returns the merged skill set. By default this is just
|
||||
// the built-in skills compiled into the zot binary; user-installed
|
||||
// SKILL.md files are NOT loaded unless includeUser is true. Users
|
||||
// opt in via the `--with-skills` flag.
|
||||
//
|
||||
// Built-in skills (compiled into the zot binary) are added LAST so
|
||||
// any user-installed skill with the same name shadows the built-in.
|
||||
// That lets users customise the help text by dropping their own
|
||||
// SKILL.md with the same name into $ZOT_HOME/skills/<name>/.
|
||||
func Discover(zotHome, cwd, userHome string) ([]*Skill, []error) {
|
||||
// First-match-wins per name; the order matches the priority list
|
||||
// in the package doc (project-local before global before claude-
|
||||
// compat before agents-compat, all before built-ins). That means a
|
||||
// user-installed skill with the same name as a built-in shadows
|
||||
// the built-in once includeUser is true.
|
||||
//
|
||||
// Errors per skill are returned alongside the partial result so a
|
||||
// single broken file doesn't suppress the rest.
|
||||
func Discover(zotHome, cwd, userHome string, includeUser bool) ([]*Skill, []error) {
|
||||
var errs []error
|
||||
seen := map[string]*Skill{}
|
||||
if includeUser {
|
||||
errs = append(errs, scanUserSkills(zotHome, cwd, userHome, seen)...)
|
||||
}
|
||||
// Built-ins fill in any name the user didn't already provide
|
||||
// (or every name, when includeUser is false).
|
||||
for _, s := range loadBuiltins() {
|
||||
if _, dup := seen[s.Name]; dup {
|
||||
continue
|
||||
}
|
||||
seen[s.Name] = s
|
||||
}
|
||||
out := make([]*Skill, 0, len(seen))
|
||||
for _, s := range seen {
|
||||
out = append(out, s)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, errs
|
||||
}
|
||||
|
||||
// scanUserSkills walks the user-skill search dirs and populates
|
||||
// `seen` with first-match-wins per name. Split out so Discover's
|
||||
// includeUser=false path doesn't have to skip over a giant block.
|
||||
func scanUserSkills(zotHome, cwd, userHome string, seen map[string]*Skill) []error {
|
||||
var errs []error
|
||||
for _, loc := range searchDirs(zotHome, cwd, userHome) {
|
||||
entries, err := os.ReadDir(loc.dir)
|
||||
if err != nil {
|
||||
|
|
@ -122,20 +149,7 @@ func Discover(zotHome, cwd, userHome string) ([]*Skill, []error) {
|
|||
seen[s.Name] = s
|
||||
}
|
||||
}
|
||||
// Built-ins fill in any name the user didn't already provide.
|
||||
for _, s := range loadBuiltins() {
|
||||
if _, dup := seen[s.Name]; dup {
|
||||
continue
|
||||
}
|
||||
seen[s.Name] = s
|
||||
}
|
||||
|
||||
out := make([]*Skill, 0, len(seen))
|
||||
for _, s := range seen {
|
||||
out = append(out, s)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out, errs
|
||||
return errs
|
||||
}
|
||||
|
||||
// SystemPromptAddendum returns the text to append to the system
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ func TestDiscoverProjectAndGlobalPriorityAndDedup(t *testing.T) {
|
|||
// Unique skill in global only.
|
||||
mk(filepath.Join(zotHome, "skills"), "global-only", "from global")
|
||||
|
||||
skills, errs := Discover(zotHome, cwd, "")
|
||||
skills, errs := Discover(zotHome, cwd, "", true /* includeUser */)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("errs: %v", errs)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue