From be1a74b2fda902e3558478df86dc189fcfbcbe7b Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Wed, 18 Feb 2026 15:53:54 +0000 Subject: [PATCH] feat(skills): add dependency graph and template rendering Introduce a skill dependency graph that resolves skill ordering and detects cycles, plus a Go template rendering system for parameterized skill definitions. Updates the skill loader to leverage both systems for progressive skill disclosure. --- pkg/skills/graph.go | 234 +++++++++++ pkg/skills/graph_test.go | 377 ++++++++++++++++++ pkg/skills/loader.go | 108 +++-- pkg/skills/templates.go | 57 +++ pkg/skills/templates/company/index/SKILL.md | 17 + .../templates/company/onboarding/SKILL.md | 43 ++ .../templates/company/org-structure/SKILL.md | 33 ++ .../templates/company/processes/SKILL.md | 33 ++ .../company/product-knowledge/SKILL.md | 31 ++ .../templates/legal/compliance/SKILL.md | 25 ++ .../legal/contract-patterns/SKILL.md | 28 ++ pkg/skills/templates/legal/index/SKILL.md | 17 + .../templates/legal/jurisdiction/SKILL.md | 29 ++ .../templates/legal/precedent-chains/SKILL.md | 30 ++ pkg/skills/templates/trading/index/SKILL.md | 23 ++ .../trading/market-psychology/SKILL.md | 32 ++ .../trading/position-sizing/SKILL.md | 50 +++ .../trading/risk-management/SKILL.md | 28 ++ .../trading/technical-analysis/SKILL.md | 33 ++ pkg/skills/templates_test.go | 63 +++ 20 files changed, 1254 insertions(+), 37 deletions(-) create mode 100644 pkg/skills/graph.go create mode 100644 pkg/skills/graph_test.go create mode 100644 pkg/skills/templates.go create mode 100644 pkg/skills/templates/company/index/SKILL.md create mode 100644 pkg/skills/templates/company/onboarding/SKILL.md create mode 100644 pkg/skills/templates/company/org-structure/SKILL.md create mode 100644 pkg/skills/templates/company/processes/SKILL.md create mode 100644 pkg/skills/templates/company/product-knowledge/SKILL.md create mode 100644 pkg/skills/templates/legal/compliance/SKILL.md create mode 100644 pkg/skills/templates/legal/contract-patterns/SKILL.md create mode 100644 pkg/skills/templates/legal/index/SKILL.md create mode 100644 pkg/skills/templates/legal/jurisdiction/SKILL.md create mode 100644 pkg/skills/templates/legal/precedent-chains/SKILL.md create mode 100644 pkg/skills/templates/trading/index/SKILL.md create mode 100644 pkg/skills/templates/trading/market-psychology/SKILL.md create mode 100644 pkg/skills/templates/trading/position-sizing/SKILL.md create mode 100644 pkg/skills/templates/trading/risk-management/SKILL.md create mode 100644 pkg/skills/templates/trading/technical-analysis/SKILL.md create mode 100644 pkg/skills/templates_test.go diff --git a/pkg/skills/graph.go b/pkg/skills/graph.go new file mode 100644 index 000000000..69f01e765 --- /dev/null +++ b/pkg/skills/graph.go @@ -0,0 +1,234 @@ +package skills + +import ( + "regexp" + "sort" + "strings" +) + +var wikilinkPattern = regexp.MustCompile(`\[\[([^\]]+)\]\]`) + +// SkillNode extends SkillInfo with graph-aware metadata extracted +// from wikilinks in the skill body and explicit links in frontmatter. +type SkillNode struct { + SkillInfo + Links []string `json:"links,omitempty"` + IsMOC bool `json:"is_moc,omitempty"` + IsIndex bool `json:"is_index,omitempty"` +} + +// SkillGraph is a directed graph of skill nodes connected by wikilinks +// and explicit frontmatter links. Edges point from a skill to the +// skills it references. +type SkillGraph struct { + Nodes map[string]*SkillNode + Edges map[string][]string +} + +// BuildGraph constructs the full skill graph from all discovered skills. +// It resolves both explicit frontmatter links and [[wikilink]] references +// found in skill content. +func (sl *SkillsLoader) BuildGraph() *SkillGraph { + g := &SkillGraph{ + Nodes: make(map[string]*SkillNode), + Edges: make(map[string][]string), + } + + allSkills := sl.ListSkills() + metaByPath := make(map[string]*SkillMetadata, len(allSkills)) + + for _, info := range allSkills { + meta := sl.getSkillMetadata(info.Path) + metaByPath[info.Path] = meta + + node := &SkillNode{SkillInfo: info} + + if meta != nil { + node.IsMOC = meta.IsMOC + node.Links = append(node.Links, meta.Links...) + } + + if strings.EqualFold(info.Name, "index") { + node.IsIndex = true + } + + g.Nodes[info.Name] = node + } + + for name, node := range g.Nodes { + content, ok := sl.LoadSkill(name) + if !ok { + continue + } + wikilinks := ParseWikilinks(content) + node.Links = mergeUnique(node.Links, wikilinks) + g.Edges[name] = node.Links + } + + return g +} + +// ParseWikilinks extracts [[skill-name]] references from markdown content. +// Returns deduplicated skill names in order of first appearance. +func ParseWikilinks(content string) []string { + matches := wikilinkPattern.FindAllStringSubmatch(content, -1) + seen := make(map[string]bool, len(matches)) + var links []string + for _, match := range matches { + if len(match) < 2 { + continue + } + name := strings.TrimSpace(match[1]) + if name == "" || seen[name] { + continue + } + seen[name] = true + links = append(links, name) + } + return links +} + +// TraverseFrom follows links from a starting node up to the given depth +// using BFS. Returns reachable nodes excluding the starting node. +func (g *SkillGraph) TraverseFrom(name string, depth int) []*SkillNode { + if depth <= 0 || g.Nodes[name] == nil { + return nil + } + + type entry struct { + name string + depth int + } + + visited := map[string]bool{name: true} + queue := []entry{{name, 0}} + var result []*SkillNode + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + + if cur.name != name { + if node := g.Nodes[cur.name]; node != nil { + result = append(result, node) + } + } + + if cur.depth >= depth { + continue + } + + for _, link := range g.Edges[cur.name] { + if visited[link] { + continue + } + visited[link] = true + queue = append(queue, entry{link, cur.depth + 1}) + } + } + + return result +} + +// SearchSkills performs fuzzy matching against skill names, descriptions, +// tags, and domains. Results are scored and returned in descending relevance. +func (g *SkillGraph) SearchSkills(query string) []*SkillNode { + query = strings.ToLower(query) + tokens := strings.Fields(query) + if len(tokens) == 0 { + return nil + } + + type scored struct { + node *SkillNode + score int + } + + var results []scored + + for _, node := range g.Nodes { + nameLower := strings.ToLower(node.Name) + descLower := strings.ToLower(node.Description) + domainLower := strings.ToLower(node.Domain) + + score := 0 + for _, token := range tokens { + if strings.Contains(nameLower, token) { + score += 3 + } + if strings.Contains(descLower, token) { + score += 2 + } + if strings.Contains(domainLower, token) { + score += 2 + } + for _, tag := range node.Tags { + if strings.Contains(strings.ToLower(tag), token) { + score += 2 + } + } + } + + if score > 0 { + results = append(results, scored{node, score}) + } + } + + sort.Slice(results, func(i, j int) bool { + return results[i].score > results[j].score + }) + + nodes := make([]*SkillNode, len(results)) + for i, r := range results { + nodes[i] = r.node + } + return nodes +} + +// LinksFrom returns direct link targets from a named skill node. +func (g *SkillGraph) LinksFrom(name string) []string { + return g.Edges[name] +} + +// GetNode returns a skill node by name, or nil if not found. +func (g *SkillGraph) GetNode(name string) *SkillNode { + return g.Nodes[name] +} + +// ListMOCs returns all Map of Content nodes in the graph. +func (g *SkillGraph) ListMOCs() []*SkillNode { + var mocs []*SkillNode + for _, node := range g.Nodes { + if node.IsMOC { + mocs = append(mocs, node) + } + } + return mocs +} + +// GetIndex returns the root index node, or nil if none exists. +func (g *SkillGraph) GetIndex() *SkillNode { + for _, node := range g.Nodes { + if node.IsIndex { + return node + } + } + return nil +} + +func mergeUnique(a, b []string) []string { + seen := make(map[string]bool, len(a)) + for _, s := range a { + seen[s] = true + } + result := make([]string, len(a)) + copy(result, a) + for _, s := range b { + if seen[s] { + continue + } + seen[s] = true + result = append(result, s) + } + return result +} diff --git a/pkg/skills/graph_test.go b/pkg/skills/graph_test.go new file mode 100644 index 000000000..3979b956d --- /dev/null +++ b/pkg/skills/graph_test.go @@ -0,0 +1,377 @@ +package skills + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseWikilinks(t *testing.T) { + tests := []struct { + name string + content string + want []string + }{ + { + name: "single link", + content: "See [[risk-management]] for details.", + want: []string{"risk-management"}, + }, + { + name: "multiple links", + content: "Use [[position-sizing]] and [[technical-analysis]] together.", + want: []string{"position-sizing", "technical-analysis"}, + }, + { + name: "duplicate links deduplicated", + content: "Read [[alpha]] first, then [[beta]], then revisit [[alpha]].", + want: []string{"alpha", "beta"}, + }, + { + name: "no links", + content: "This has no wikilinks at all.", + want: nil, + }, + { + name: "empty content", + content: "", + want: nil, + }, + { + name: "link with spaces trimmed", + content: "See [[ spaced-link ]] here.", + want: []string{"spaced-link"}, + }, + { + name: "nested brackets ignored", + content: "Not a link: [not [real]] but [[actual-link]] is.", + want: []string{"actual-link"}, + }, + { + name: "multiline content", + content: "# Title\n\nSee [[skill-a]].\n\nAlso [[skill-b]] and [[skill-c]].\n", + want: []string{"skill-a", "skill-b", "skill-c"}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := ParseWikilinks(tc.content) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestMergeUnique(t *testing.T) { + tests := []struct { + name string + a, b []string + want []string + }{ + {"both empty", nil, nil, []string{}}, + {"a only", []string{"x"}, nil, []string{"x"}}, + {"b only", nil, []string{"y"}, []string{"y"}}, + {"overlap", []string{"a", "b"}, []string{"b", "c"}, []string{"a", "b", "c"}}, + {"no overlap", []string{"a"}, []string{"b"}, []string{"a", "b"}}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := mergeUnique(tc.a, tc.b) + assert.Equal(t, tc.want, got) + }) + } +} + +func writeSkill(t *testing.T, dir, name, content string) { + t.Helper() + skillDir := filepath.Join(dir, name) + require.NoError(t, os.MkdirAll(skillDir, 0755)) + require.NoError(t, os.WriteFile(filepath.Join(skillDir, "SKILL.md"), []byte(content), 0644)) +} + +func TestBuildGraph_Basic(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "risk-management", `--- +name: risk-management +description: "Risk management fundamentals" +tags: trading, risk +domain: finance +--- +# Risk Management + +See [[position-sizing]] for sizing rules. +Also check [[technical-analysis]]. +`) + + writeSkill(t, tmp, "position-sizing", `--- +name: position-sizing +description: "Position sizing strategies" +tags: trading, sizing +domain: finance +--- +# Position Sizing + +Based on [[risk-management]] principles. +`) + + writeSkill(t, tmp, "technical-analysis", `--- +name: technical-analysis +description: "Technical analysis patterns" +tags: trading, charts +domain: finance +--- +# Technical Analysis + +No wikilinks here. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + assert.Len(t, g.Nodes, 3) + + rm := g.GetNode("risk-management") + require.NotNil(t, rm) + assert.Equal(t, []string{"trading", "risk"}, rm.Tags) + assert.Equal(t, "finance", rm.Domain) + assert.Contains(t, rm.Links, "position-sizing") + assert.Contains(t, rm.Links, "technical-analysis") + + ps := g.GetNode("position-sizing") + require.NotNil(t, ps) + assert.Contains(t, ps.Links, "risk-management") + + ta := g.GetNode("technical-analysis") + require.NotNil(t, ta) + assert.Empty(t, ta.Links) +} + +func TestBuildGraph_FrontmatterLinksAndWikilinks(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "alpha", `--- +name: alpha +description: "Alpha skill" +links: beta +--- +Content with [[gamma]] link. +`) + + writeSkill(t, tmp, "beta", `--- +name: beta +description: "Beta skill" +--- +No links. +`) + + writeSkill(t, tmp, "gamma", `--- +name: gamma +description: "Gamma skill" +--- +No links. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + alpha := g.GetNode("alpha") + require.NotNil(t, alpha) + assert.Contains(t, alpha.Links, "beta") + assert.Contains(t, alpha.Links, "gamma") +} + +func TestTraverseFrom(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "a", `--- +name: a +description: "Start node" +--- +Links to [[b]] and [[c]]. +`) + writeSkill(t, tmp, "b", `--- +name: b +description: "Mid node" +--- +Links to [[d]]. +`) + writeSkill(t, tmp, "c", `--- +name: c +description: "Leaf node" +--- +No outgoing links. +`) + writeSkill(t, tmp, "d", `--- +name: d +description: "Deep node" +--- +No outgoing links. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + depth1 := g.TraverseFrom("a", 1) + names1 := nodeNames(depth1) + assert.Contains(t, names1, "b") + assert.Contains(t, names1, "c") + assert.NotContains(t, names1, "d") + + depth2 := g.TraverseFrom("a", 2) + names2 := nodeNames(depth2) + assert.Contains(t, names2, "b") + assert.Contains(t, names2, "c") + assert.Contains(t, names2, "d") + + assert.Nil(t, g.TraverseFrom("a", 0)) + assert.Nil(t, g.TraverseFrom("nonexistent", 1)) +} + +func TestSearchSkills(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "risk-management", `--- +name: risk-management +description: "Risk management fundamentals for trading" +tags: trading, risk +domain: finance +--- +Content. +`) + writeSkill(t, tmp, "code-review", `--- +name: code-review +description: "Code review best practices" +tags: engineering +domain: software +--- +Content. +`) + writeSkill(t, tmp, "market-psychology", `--- +name: market-psychology +description: "Psychology of markets and trading behavior" +tags: trading, psychology +domain: finance +--- +Content. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + results := g.SearchSkills("trading") + require.True(t, len(results) >= 2) + names := nodeNames(skillNodesToSlice(results)) + assert.Contains(t, names, "risk-management") + assert.Contains(t, names, "market-psychology") + + results = g.SearchSkills("software engineering") + require.True(t, len(results) >= 1) + assert.Equal(t, "code-review", results[0].Name) + + results = g.SearchSkills("") + assert.Empty(t, results) +} + +func TestListMOCs(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "trading-moc", `--- +name: trading-moc +description: "Map of trading skills" +is_moc: true +domain: finance +--- +Overview of [[risk-management]] and [[position-sizing]]. +`) + writeSkill(t, tmp, "risk-management", `--- +name: risk-management +description: "Risk basics" +--- +Content. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + mocs := g.ListMOCs() + assert.Len(t, mocs, 1) + assert.Equal(t, "trading-moc", mocs[0].Name) + assert.True(t, mocs[0].IsMOC) +} + +func TestGetIndex(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "index", `--- +name: index +description: "Root index of the skill graph" +--- +Start here: [[trading-moc]], [[engineering-moc]]. +`) + writeSkill(t, tmp, "trading-moc", `--- +name: trading-moc +description: "Trading overview" +--- +Content. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + idx := g.GetIndex() + require.NotNil(t, idx) + assert.Equal(t, "index", idx.Name) + assert.True(t, idx.IsIndex) + assert.Contains(t, idx.Links, "trading-moc") + assert.Contains(t, idx.Links, "engineering-moc") +} + +func TestGetIndex_None(t *testing.T) { + tmp := t.TempDir() + writeSkill(t, tmp, "some-skill", `--- +name: some-skill +description: "Not an index" +--- +Content. +`) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + assert.Nil(t, g.GetIndex()) +} + +func TestExtendedFrontmatter_JSON(t *testing.T) { + tmp := t.TempDir() + + writeSkill(t, tmp, "json-skill", `--- +{"name":"json-skill","description":"A JSON frontmatter skill","tags":["alpha","beta"],"domain":"testing","is_moc":true} +--- +Content with [[some-link]]. +`) + + sl := NewSkillsLoader("", "", tmp) + skills := sl.ListSkills() + require.Len(t, skills, 1) + + s := skills[0] + assert.Equal(t, "json-skill", s.Name) + assert.Equal(t, []string{"alpha", "beta"}, s.Tags) + assert.Equal(t, "testing", s.Domain) +} + +func nodeNames(nodes []*SkillNode) []string { + names := make([]string, len(nodes)) + for i, n := range nodes { + names[i] = n.Name + } + return names +} + +func skillNodesToSlice(nodes []*SkillNode) []*SkillNode { + return nodes +} diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index 0c63ae067..7eeed8bff 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -19,15 +19,21 @@ const ( ) type SkillMetadata struct { - Name string `json:"name"` - Description string `json:"description"` + Name string `json:"name"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` + Links []string `json:"links,omitempty"` + Domain string `json:"domain,omitempty"` + IsMOC bool `json:"is_moc,omitempty"` } type SkillInfo struct { - Name string `json:"name"` - Path string `json:"path"` - Source string `json:"source"` - Description string `json:"description"` + Name string `json:"name"` + Path string `json:"path"` + Source string `json:"source"` + Description string `json:"description"` + Tags []string `json:"tags,omitempty"` + Domain string `json:"domain,omitempty"` } func (info SkillInfo) validate() error { @@ -85,6 +91,8 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { if metadata != nil { info.Description = metadata.Description info.Name = metadata.Name + info.Tags = metadata.Tags + info.Domain = metadata.Domain } if err := info.validate(); err != nil { slog.Warn("invalid skill from workspace", "name", info.Name, "error", err) @@ -116,16 +124,18 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { continue } - info := SkillInfo{ - Name: dir.Name(), - Path: skillFile, - Source: "global", - } - metadata := sl.getSkillMetadata(skillFile) - if metadata != nil { - info.Description = metadata.Description - info.Name = metadata.Name - } + info := SkillInfo{ + Name: dir.Name(), + Path: skillFile, + Source: "global", + } + metadata := sl.getSkillMetadata(skillFile) + if metadata != nil { + info.Description = metadata.Description + info.Name = metadata.Name + info.Tags = metadata.Tags + info.Domain = metadata.Domain + } if err := info.validate(); err != nil { slog.Warn("invalid skill from global", "name", info.Name, "error", err) continue @@ -155,16 +165,18 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { continue } - info := SkillInfo{ - Name: dir.Name(), - Path: skillFile, - Source: "builtin", - } - metadata := sl.getSkillMetadata(skillFile) - if metadata != nil { - info.Description = metadata.Description - info.Name = metadata.Name - } + info := SkillInfo{ + Name: dir.Name(), + Path: skillFile, + Source: "builtin", + } + metadata := sl.getSkillMetadata(skillFile) + if metadata != nil { + info.Description = metadata.Description + info.Name = metadata.Name + info.Tags = metadata.Tags + info.Domain = metadata.Domain + } if err := info.validate(); err != nil { slog.Warn("invalid skill from builtin", "name", info.Name, "error", err) continue @@ -236,11 +248,17 @@ func (sl *SkillsLoader) BuildSkillsSummary() string { escapedDesc := escapeXML(s.Description) escapedPath := escapeXML(s.Path) - lines = append(lines, fmt.Sprintf(" ")) + lines = append(lines, " ") lines = append(lines, fmt.Sprintf(" %s", escapedName)) lines = append(lines, fmt.Sprintf(" %s", escapedDesc)) lines = append(lines, fmt.Sprintf(" %s", escapedPath)) lines = append(lines, fmt.Sprintf(" %s", s.Source)) + if len(s.Tags) > 0 { + lines = append(lines, fmt.Sprintf(" %s", escapeXML(strings.Join(s.Tags, ", ")))) + } + if s.Domain != "" { + lines = append(lines, fmt.Sprintf(" %s", escapeXML(s.Domain))) + } lines = append(lines, " ") } lines = append(lines, "") @@ -262,23 +280,28 @@ func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata { } // Try JSON first (for backward compatibility) - var jsonMeta struct { - Name string `json:"name"` - Description string `json:"description"` - } - if err := json.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil { - return &SkillMetadata{ - Name: jsonMeta.Name, - Description: jsonMeta.Description, - } + var jsonMeta SkillMetadata + if err := json.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil && jsonMeta.Name != "" { + return &jsonMeta } // Fall back to simple YAML parsing yamlMeta := sl.parseSimpleYAML(frontmatter) - return &SkillMetadata{ + meta := &SkillMetadata{ Name: yamlMeta["name"], Description: yamlMeta["description"], + Domain: yamlMeta["domain"], } + if tags := yamlMeta["tags"]; tags != "" { + meta.Tags = splitCSV(tags) + } + if links := yamlMeta["links"]; links != "" { + meta.Links = splitCSV(links) + } + if yamlMeta["is_moc"] == "true" { + meta.IsMOC = true + } + return meta } // parseSimpleYAML parses simple key: value YAML format @@ -321,6 +344,17 @@ func (sl *SkillsLoader) stripFrontmatter(content string) string { return re.ReplaceAllString(content, "") } +func splitCSV(s string) []string { + var result []string + for _, item := range strings.Split(s, ",") { + item = strings.TrimSpace(item) + if item != "" { + result = append(result, item) + } + } + return result +} + func escapeXML(s string) string { s = strings.ReplaceAll(s, "&", "&") s = strings.ReplaceAll(s, "<", "<") diff --git a/pkg/skills/templates.go b/pkg/skills/templates.go new file mode 100644 index 000000000..fdcfb2083 --- /dev/null +++ b/pkg/skills/templates.go @@ -0,0 +1,57 @@ +package skills + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path/filepath" +) + +//go:embed templates/* +var templateFS embed.FS + +// AvailableTemplates returns the names of all embedded domain skill graph templates. +func AvailableTemplates() []string { + entries, err := fs.ReadDir(templateFS, "templates") + if err != nil { + return nil + } + var names []string + for _, e := range entries { + if e.IsDir() { + names = append(names, e.Name()) + } + } + return names +} + +// InstallTemplate copies an embedded domain skill graph template into the +// target skills directory. Each subdirectory under templates// becomes +// a skill directory containing a SKILL.md. +func InstallTemplate(templateName, targetSkillsDir string) error { + root := filepath.Join("templates", templateName) + if _, err := fs.Stat(templateFS, root); err != nil { + return fmt.Errorf("template '%s' not found (available: %v)", templateName, AvailableTemplates()) + } + + return fs.WalkDir(templateFS, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, _ := filepath.Rel(root, path) + dest := filepath.Join(targetSkillsDir, rel) + + if d.IsDir() { + return os.MkdirAll(dest, 0755) + } + + data, err := templateFS.ReadFile(path) + if err != nil { + return fmt.Errorf("read embedded %s: %w", path, err) + } + + return os.WriteFile(dest, data, 0644) + }) +} diff --git a/pkg/skills/templates/company/index/SKILL.md b/pkg/skills/templates/company/index/SKILL.md new file mode 100644 index 000000000..51f62a88d --- /dev/null +++ b/pkg/skills/templates/company/index/SKILL.md @@ -0,0 +1,17 @@ +--- +name: index +description: "Root index of the company knowledge graph — organizational knowledge base" +domain: company +is_moc: true +--- + +# Company Skill Graph + +Organizational knowledge organized for quick onboarding and reference. + +## Core Domains + +- [[org-structure]] — Teams, reporting lines, key stakeholders, RACI matrices +- [[product-knowledge]] — Products, features, roadmap, competitive landscape +- [[processes]] — Development workflows, release process, incident response +- [[onboarding]] — New hire guide, access requests, environment setup diff --git a/pkg/skills/templates/company/onboarding/SKILL.md b/pkg/skills/templates/company/onboarding/SKILL.md new file mode 100644 index 000000000..371014ea1 --- /dev/null +++ b/pkg/skills/templates/company/onboarding/SKILL.md @@ -0,0 +1,43 @@ +--- +name: onboarding +description: "New hire onboarding — access, environment setup, first-week guide" +tags: company, onboarding, setup, new-hire +domain: company +links: org-structure, processes, product-knowledge +--- + +# Onboarding + +## Day 1 + +- [ ] Get laptop and credentials +- [ ] Set up email, Slack/Teams, calendar +- [ ] Request access: GitHub, CI/CD, cloud console, monitoring dashboards +- [ ] Clone main repositories and verify build + +## Week 1 + +- [ ] Read [[product-knowledge]] to understand what we build and why +- [ ] Read [[org-structure]] to understand teams and reporting +- [ ] Read [[processes]] to understand how we ship +- [ ] Complete a "good first issue" to practice the full workflow +- [ ] Shadow an on-call shift to understand production + +## Environment Setup + +```bash +# Clone and build (customize for your stack) +git clone +cd +make setup # installs dependencies +make build # verifies compilation +make test # runs test suite +``` + +## Key Contacts + +| Role | Name | When to reach out | +|------|------|-------------------| +| Buddy | [Assigned] | Any question, no matter how small | +| Team Lead | [Name] | Technical direction, priorities | +| HR | [Name] | Benefits, policies, admin | diff --git a/pkg/skills/templates/company/org-structure/SKILL.md b/pkg/skills/templates/company/org-structure/SKILL.md new file mode 100644 index 000000000..dd5a974ea --- /dev/null +++ b/pkg/skills/templates/company/org-structure/SKILL.md @@ -0,0 +1,33 @@ +--- +name: org-structure +description: "Organizational structure — teams, roles, reporting lines, and stakeholders" +tags: company, organization, teams, roles +domain: company +links: processes, onboarding +--- + +# Organizational Structure + +## Template + +Fill in your organization's specifics: + +### Engineering +- **Engineering Lead**: [Name] +- **Teams**: [Platform, Product, Infrastructure, ...] +- **Team leads**: [Names and areas of ownership] + +### Product +- **Product Lead**: [Name] +- **Product areas**: [Core, Growth, Platform, ...] + +### Key Stakeholders (RACI) + +| Decision | Responsible | Accountable | Consulted | Informed | +|----------|------------|-------------|-----------|----------| +| Architecture | Eng Lead | CTO | Team Leads | All Eng | +| Roadmap | PM Lead | CEO | Eng Lead | All | +| Hiring | Hiring Mgr | Dept Head | Team | HR | + +See [[processes]] for how teams coordinate on delivery. +See [[onboarding]] for how new members join teams. diff --git a/pkg/skills/templates/company/processes/SKILL.md b/pkg/skills/templates/company/processes/SKILL.md new file mode 100644 index 000000000..bf2611fbc --- /dev/null +++ b/pkg/skills/templates/company/processes/SKILL.md @@ -0,0 +1,33 @@ +--- +name: processes +description: "Development workflows, release process, and incident response procedures" +tags: company, process, workflow, release, incident +domain: company +links: org-structure, product-knowledge +--- + +# Processes + +## Development Workflow + +1. **Planning** — tickets created from roadmap priorities +2. **Development** — branch from main, implement, write tests +3. **Review** — PR with at least one approval required +4. **QA** — automated tests + manual verification for critical paths +5. **Deploy** — staged rollout: staging → canary → production + +## Release Process + +- **Cadence**: [Weekly/Biweekly/Continuous] +- **Cut**: [Day/time] +- **Rollback**: [Procedure and decision criteria] + +## Incident Response + +1. **Detect** — alerts fire, customer reports, monitoring anomalies +2. **Triage** — assess severity (P0-P3), assign incident commander +3. **Mitigate** — restore service; fix root cause later +4. **Communicate** — status page updates, stakeholder notifications +5. **Postmortem** — blameless analysis within 48 hours; action items tracked to completion + +See [[org-structure]] for escalation paths and on-call rotations. diff --git a/pkg/skills/templates/company/product-knowledge/SKILL.md b/pkg/skills/templates/company/product-knowledge/SKILL.md new file mode 100644 index 000000000..a130fd5b5 --- /dev/null +++ b/pkg/skills/templates/company/product-knowledge/SKILL.md @@ -0,0 +1,31 @@ +--- +name: product-knowledge +description: "Product knowledge — features, roadmap, competitive landscape" +tags: company, product, roadmap, features +domain: company +links: processes +--- + +# Product Knowledge + +## Template + +### Products + +| Product | Description | Target User | Status | +|---------|-------------|-------------|--------| +| [Name] | [What it does] | [Who uses it] | [GA/Beta/Alpha] | + +### Key Features +- [Feature 1]: [Description and value proposition] +- [Feature 2]: [Description and value proposition] + +### Competitive Landscape +- **Competitor A**: [Strengths, weaknesses, differentiation] +- **Competitor B**: [Strengths, weaknesses, differentiation] + +### Roadmap Themes +- **Current quarter**: [Theme and key initiatives] +- **Next quarter**: [Planned themes] + +See [[processes]] for how product decisions flow into engineering. diff --git a/pkg/skills/templates/legal/compliance/SKILL.md b/pkg/skills/templates/legal/compliance/SKILL.md new file mode 100644 index 000000000..b9a8f2ed4 --- /dev/null +++ b/pkg/skills/templates/legal/compliance/SKILL.md @@ -0,0 +1,25 @@ +--- +name: compliance +description: "Regulatory compliance frameworks, audit preparation, and reporting" +tags: legal, compliance, regulation, audit +domain: legal +links: contract-patterns +--- + +# Compliance + +## Common Frameworks + +- **GDPR** — data protection for EU subjects; consent, right to erasure, DPA requirements +- **SOC 2** — trust service criteria: security, availability, processing integrity, confidentiality, privacy +- **PCI-DSS** — payment card data handling; network segmentation, encryption, access control +- **HIPAA** — protected health information; BAAs, minimum necessary standard, breach notification + +## Audit Preparation + +- Maintain evidence logs continuously (not just before audits) +- Map controls to framework requirements with a control matrix +- Document exceptions and compensating controls +- Track remediation timelines with assigned owners + +See [[contract-patterns]] for required compliance clauses in vendor agreements. diff --git a/pkg/skills/templates/legal/contract-patterns/SKILL.md b/pkg/skills/templates/legal/contract-patterns/SKILL.md new file mode 100644 index 000000000..673ea0be0 --- /dev/null +++ b/pkg/skills/templates/legal/contract-patterns/SKILL.md @@ -0,0 +1,28 @@ +--- +name: contract-patterns +description: "Common contract clause structures, negotiation points, and red flags" +tags: legal, contracts, negotiation +domain: legal +links: compliance, jurisdiction +--- + +# Contract Patterns + +## Essential Clauses + +- **Limitation of liability** — cap damages at contract value; carve out gross negligence/IP indemnity +- **Indemnification** — mutual vs. one-way; trigger events; defense obligations +- **Termination** — for cause (material breach + cure period) vs. for convenience (notice period) +- **Force majeure** — enumerated events; mitigation obligations; termination rights after extended force majeure +- **IP ownership** — work-for-hire vs. license-back; pre-existing IP carved out + +## Red Flags + +- Unlimited liability exposure +- One-sided termination rights without cure period +- Automatic renewal without notice windows +- Broad non-compete extending beyond the contract scope +- Vague "all intellectual property" assignment clauses + +See [[compliance]] for regulatory requirements that affect contract terms. +See [[jurisdiction]] for governing law and dispute resolution clauses. diff --git a/pkg/skills/templates/legal/index/SKILL.md b/pkg/skills/templates/legal/index/SKILL.md new file mode 100644 index 000000000..59ac80494 --- /dev/null +++ b/pkg/skills/templates/legal/index/SKILL.md @@ -0,0 +1,17 @@ +--- +name: index +description: "Root index of the legal skill graph — navigate legal knowledge domains" +domain: legal +is_moc: true +--- + +# Legal Skill Graph + +Interconnected legal knowledge organized by practice area. + +## Core Domains + +- [[contract-patterns]] — Common clause structures, negotiation points, red flags +- [[compliance]] — Regulatory frameworks, audit preparation, reporting requirements +- [[jurisdiction]] — Venue selection, conflict of laws, cross-border considerations +- [[precedent-chains]] — Case law research, citation patterns, persuasive authority diff --git a/pkg/skills/templates/legal/jurisdiction/SKILL.md b/pkg/skills/templates/legal/jurisdiction/SKILL.md new file mode 100644 index 000000000..d14e0d89c --- /dev/null +++ b/pkg/skills/templates/legal/jurisdiction/SKILL.md @@ -0,0 +1,29 @@ +--- +name: jurisdiction +description: "Venue selection, conflict of laws, and cross-border considerations" +tags: legal, jurisdiction, venue, international +domain: legal +links: contract-patterns, compliance +--- + +# Jurisdiction + +## Governing Law Selection + +- Choose a jurisdiction with well-developed commercial law (Delaware, England & Wales, Singapore) +- Consider where parties are located, where performance occurs, and where disputes would be enforced +- Mandatory local laws override choice-of-law clauses (consumer protection, employment, data privacy) + +## Dispute Resolution + +- **Litigation** — public record, full discovery, appeal rights; expensive and slow +- **Arbitration** — private, limited discovery, final and binding; ICC, LCIA, AAA/ICDR +- **Mediation** — non-binding facilitated negotiation; often a prerequisite before arbitration + +## Cross-Border + +- Understand enforcement mechanisms (New York Convention for arbitral awards) +- Account for currency, language, and cultural differences in drafting +- Map regulatory requirements per [[compliance]] for each jurisdiction involved + +See [[contract-patterns]] for governing law and dispute resolution clause templates. diff --git a/pkg/skills/templates/legal/precedent-chains/SKILL.md b/pkg/skills/templates/legal/precedent-chains/SKILL.md new file mode 100644 index 000000000..4ca1d7103 --- /dev/null +++ b/pkg/skills/templates/legal/precedent-chains/SKILL.md @@ -0,0 +1,30 @@ +--- +name: precedent-chains +description: "Case law research patterns, citation analysis, and persuasive authority" +tags: legal, research, case-law, precedent +domain: legal +links: jurisdiction +--- + +# Precedent Chains + +## Research Strategy + +1. Start with a key case or statute +2. Shepardize/KeyCite to find citing cases and check validity +3. Map the citation chain: which cases cite which, in what context +4. Identify the binding hierarchy: Supreme Court > Circuit > District > State + +## Citation Patterns + +- **Positive treatment** — followed, affirmed, approved +- **Negative treatment** — overruled, distinguished, criticized +- **Neutral treatment** — cited, discussed, mentioned + +## Persuasive Authority + +- Sister circuit opinions (strong persuasion in absence of binding authority) +- Restatements and treatises (secondary authority with high credibility) +- Law review articles (persuasive for novel questions) + +See [[jurisdiction]] for understanding which courts' decisions are binding. diff --git a/pkg/skills/templates/trading/index/SKILL.md b/pkg/skills/templates/trading/index/SKILL.md new file mode 100644 index 000000000..23eac7ac2 --- /dev/null +++ b/pkg/skills/templates/trading/index/SKILL.md @@ -0,0 +1,23 @@ +--- +name: index +description: "Root index of the trading skill graph — start here to navigate all trading knowledge" +domain: finance +is_moc: true +--- + +# Trading Skill Graph + +This graph organizes trading knowledge into interconnected domains. Follow the links to explore. + +## Core Domains + +- [[risk-management]] — Position sizing, stop losses, risk/reward ratios +- [[technical-analysis]] — Chart patterns, indicators, price action +- [[market-psychology]] — Emotional discipline, cognitive biases, crowd behavior +- [[position-sizing]] — Kelly criterion, fixed fractional, volatility-based sizing + +## How to Navigate + +1. Use `skill_search` to find skills by topic +2. Use `skill_read` to load a specific skill +3. Use `skill_traverse` to explore connections from any node diff --git a/pkg/skills/templates/trading/market-psychology/SKILL.md b/pkg/skills/templates/trading/market-psychology/SKILL.md new file mode 100644 index 000000000..7da1a0740 --- /dev/null +++ b/pkg/skills/templates/trading/market-psychology/SKILL.md @@ -0,0 +1,32 @@ +--- +name: market-psychology +description: "Psychology of trading — managing emotions and cognitive biases" +tags: trading, psychology, discipline +domain: finance +links: risk-management +--- + +# Market Psychology + +## Cognitive Biases in Trading + +- **Loss aversion** — losses feel 2x worse than equivalent gains; causes holding losers too long +- **Confirmation bias** — seeking information that supports your existing position +- **Recency bias** — overweighting recent events; the last trade dominates thinking +- **Anchoring** — fixating on entry price instead of current market reality +- **Sunk cost fallacy** — refusing to exit because of how much is already invested + +## Emotional Discipline + +- **Trade the plan, not the emotion** — predefined entries, exits, and position sizes +- **Journal every trade** — record reasoning, emotions, outcome; review weekly +- **Accept losses as cost of doing business** — each trade is one of a thousand +- **Walk away rule** — after 3 consecutive losses, stop trading for the day + +## Crowd Psychology + +- **Fear and greed cycles** — extremes signal potential reversals +- **Sentiment indicators** — VIX, put/call ratio, fund flows +- **Contrarian signals** — when everyone agrees, the move is likely over + +Apply [[risk-management]] rules mechanically to remove emotion from execution. diff --git a/pkg/skills/templates/trading/position-sizing/SKILL.md b/pkg/skills/templates/trading/position-sizing/SKILL.md new file mode 100644 index 000000000..dd6313791 --- /dev/null +++ b/pkg/skills/templates/trading/position-sizing/SKILL.md @@ -0,0 +1,50 @@ +--- +name: position-sizing +description: "Position sizing strategies — how much capital to allocate per trade" +tags: trading, sizing, money-management +domain: finance +links: risk-management +--- + +# Position Sizing + +## Methods + +### Fixed Fractional + +Risk a fixed percentage of account equity per trade. + +``` +Position Size = (Account * Risk%) / (Entry - Stop) +``` + +Example: $100K account, 1% risk, $50 entry, $48 stop: +Position = ($100K * 0.01) / ($50 - $48) = 500 shares + +### Kelly Criterion + +Optimal fraction based on win rate and payoff ratio: + +``` +Kelly% = W - (1-W)/R +``` + +Where W = win rate, R = avg win / avg loss. **Use half-Kelly in practice** to account for estimation error. + +### Volatility-Based (ATR) + +Scale position size inversely with volatility: + +``` +Position Size = (Account * Risk%) / (N * ATR) +``` + +Where N is a multiplier (typically 1.5-2.0). + +## Rules + +- Never exceed 5% of account in a single position +- Scale into winners, not losers +- Reduce size during drawdowns; increase as equity grows + +See [[risk-management]] for the broader risk framework. diff --git a/pkg/skills/templates/trading/risk-management/SKILL.md b/pkg/skills/templates/trading/risk-management/SKILL.md new file mode 100644 index 000000000..db192fc08 --- /dev/null +++ b/pkg/skills/templates/trading/risk-management/SKILL.md @@ -0,0 +1,28 @@ +--- +name: risk-management +description: "Risk management fundamentals — never risk more than you can afford to lose" +tags: trading, risk, money-management +domain: finance +links: position-sizing, market-psychology +--- + +# Risk Management + +## Core Principles + +- **Never risk more than 1-2% of account per trade** — survival comes first +- **Risk/Reward ratio** — minimum 1:2 R/R before entering; 1:3 preferred +- **Correlation risk** — multiple positions in the same sector multiply risk +- **Maximum daily drawdown** — stop trading after hitting your daily loss limit + +## Stop Loss Strategies + +- **Fixed percentage** — set stop at 1-2% of entry price +- **ATR-based** — 1.5-2x ATR for volatility-adjusted stops +- **Structure-based** — place stops below/above key support/resistance levels +- **Time-based** — exit if the trade doesn't move in your favor within N bars + +## Related Skills + +See [[position-sizing]] for how much capital to allocate per trade. +See [[market-psychology]] for managing the emotional side of risk. diff --git a/pkg/skills/templates/trading/technical-analysis/SKILL.md b/pkg/skills/templates/trading/technical-analysis/SKILL.md new file mode 100644 index 000000000..46b8b297b --- /dev/null +++ b/pkg/skills/templates/trading/technical-analysis/SKILL.md @@ -0,0 +1,33 @@ +--- +name: technical-analysis +description: "Technical analysis patterns — reading price action and chart patterns" +tags: trading, charts, indicators, price-action +domain: finance +links: risk-management +--- + +# Technical Analysis + +## Price Action + +- **Support/Resistance** — horizontal levels where price has historically reversed +- **Trend lines** — diagonal support/resistance connecting swing highs or lows +- **Candlestick patterns** — engulfing, doji, hammer, shooting star signal reversals + +## Key Indicators + +- **Moving averages** — 20 EMA for trend, 50/200 SMA for direction +- **RSI** — oversold below 30, overbought above 70; divergence signals reversals +- **Volume** — confirms breakouts; lack of volume = suspect move +- **MACD** — signal line crossovers for momentum shifts + +## Chart Patterns + +- **Head and shoulders** — reversal pattern after uptrend +- **Double top/bottom** — reversal at key levels +- **Flags and pennants** — continuation patterns in strong trends +- **Cup and handle** — bullish continuation + +## Integration + +Always combine TA signals with [[risk-management]] rules before entering a trade. diff --git a/pkg/skills/templates_test.go b/pkg/skills/templates_test.go new file mode 100644 index 000000000..424e071d6 --- /dev/null +++ b/pkg/skills/templates_test.go @@ -0,0 +1,63 @@ +package skills + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAvailableTemplates(t *testing.T) { + templates := AvailableTemplates() + assert.Contains(t, templates, "trading") + assert.Contains(t, templates, "legal") + assert.Contains(t, templates, "company") +} + +func TestInstallTemplate(t *testing.T) { + tmp := t.TempDir() + + err := InstallTemplate("trading", tmp) + require.NoError(t, err) + + indexSkill := filepath.Join(tmp, "index", "SKILL.md") + assert.FileExists(t, indexSkill) + + riskSkill := filepath.Join(tmp, "risk-management", "SKILL.md") + assert.FileExists(t, riskSkill) + + content, err := os.ReadFile(riskSkill) + require.NoError(t, err) + assert.Contains(t, string(content), "risk-management") + assert.Contains(t, string(content), "[[position-sizing]]") +} + +func TestInstallTemplate_BuildsValidGraph(t *testing.T) { + tmp := t.TempDir() + require.NoError(t, InstallTemplate("trading", tmp)) + + sl := NewSkillsLoader("", "", tmp) + g := sl.BuildGraph() + + assert.True(t, len(g.Nodes) >= 4, "trading template should have at least 4 skills") + + idx := g.GetIndex() + require.NotNil(t, idx, "trading template should have an index node") + assert.True(t, idx.IsIndex) + assert.True(t, idx.IsMOC) + assert.True(t, len(idx.Links) >= 3, "index should link to at least 3 skills") + + rm := g.GetNode("risk-management") + require.NotNil(t, rm) + assert.Equal(t, "finance", rm.Domain) + assert.Contains(t, rm.Tags, "trading") +} + +func TestInstallTemplate_NotFound(t *testing.T) { + tmp := t.TempDir() + err := InstallTemplate("nonexistent", tmp) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found") +}