返回 DeepSeek-Reasonix
profile.go
根目录 / internal / skill / profile.go
1 package skill
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8
9 "reasonix/internal/config"
10 "reasonix/internal/frontmatter"
11 )
12
13 var reservedSubagentSlashNames = map[string]bool{
14 "new": true, "clear": true, "compact": true, "model": true, "provider": true,
15 "effort": true, "memory": true, "migrate": true, "migration": true,
16 "goal": true, "remember": true, "mcp": true, "hooks": true, "plugin": true, "plugins": true,
17 "theme": true, "skill": true, "skills": true, "reload-cmd": true, "tree": true,
18 "branch": true, "switch": true, "rewind": true, "plan-exec": true, "prometheus": true,
19 "resume": true, "rename": true, "todo": true, "verbose": true, "mouse": true,
20 "sandbox": true, "work-mode": true, "profile": true,
21 "reasoning-language": true, "paste-image": true, "output-style": true,
22 "output-styles": true, "diff-fold": true, "language": true, "help": true,
23 "quit": true, "exit": true, "copy": true, "export": true, "forget": true,
24 }
25
26 // ValidateSubagentProfileName protects the shared slash namespace used by
27 // built-in verbs, custom commands, MCP prompts, Skills, and Subagents.
28 func ValidateSubagentProfileName(name string, occupied []string) error {
29 if !IsValidName(name) {
30 return fmt.Errorf("invalid name %q — use letters, digits, '_', '-', '.'", name)
31 }
32 key := config.SkillNameKey(name)
33 if reservedSubagentSlashNames[key] || strings.HasPrefix(key, "mcp__") {
34 return fmt.Errorf("%q is reserved by the slash command namespace", name)
35 }
36 for _, existing := range occupied {
37 if config.SkillNameKey(existing) == key {
38 return fmt.Errorf("%q already exists in the slash command namespace", name)
39 }
40 }
41 return nil
42 }
43
44 // subagentProfileManagedKeys are the frontmatter keys the desktop and CLI
45 // profile editors can round-trip without changing execution semantics.
46 var subagentProfileManagedKeys = map[string]bool{
47 "name": true, "description": true, "color": true, "invocation": true,
48 "runas": true, "model": true, "effort": true, "read-only": true, "allowed-tools": true,
49 }
50
51 // ValidateEditableSubagentProfile verifies that a loaded skill is a manual
52 // subagent profile whose backing file can be losslessly rewritten by the
53 // profile editors. Rich hand-authored skills remain owned by the Skills
54 // workflow so read-only, routing, references, and scripts are never dropped.
55 func ValidateEditableSubagentProfile(sk Skill) error {
56 if sk.RunAs != RunSubagent {
57 return fmt.Errorf("%q is not a subagent profile (runAs is not \"subagent\") — manage it as a skill file instead", sk.Name)
58 }
59 if sk.Invocation != "manual" {
60 return fmt.Errorf("%q was not created by a subagent profile editor (invocation is not \"manual\") — manage it as a skill file instead", sk.Name)
61 }
62 if sk.Scope != ScopeProject && sk.Scope != ScopeGlobal {
63 return fmt.Errorf("%q is scope %q and cannot be edited as a project/global subagent profile", sk.Name, sk.Scope)
64 }
65 if sk.Path == "" || sk.Path == "(builtin)" {
66 return fmt.Errorf("%q has no editable file", sk.Name)
67 }
68 raw, err := os.ReadFile(sk.Path)
69 if err != nil {
70 return err
71 }
72 fm, _ := frontmatter.Split(string(raw))
73 for key := range fm {
74 if !subagentProfileManagedKeys[key] {
75 return fmt.Errorf("%q carries frontmatter this editor does not manage (%s) and would silently drop — edit it as a skill file instead", sk.Name, key)
76 }
77 }
78 if filepath.Base(sk.Path) == SkillFile {
79 dir := filepath.Dir(sk.Path)
80 for _, sibling := range []string{"references", "scripts"} {
81 if info, err := os.Stat(filepath.Join(dir, sibling)); err == nil && info.IsDir() {
82 return fmt.Errorf("%q has a %s/ directory whose content is folded into the body at load time — editing here would bake it into the main file; edit it as a skill file instead", sk.Name, sibling)
83 }
84 }
85 }
86 return nil
87 }
88
88 lines GO