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.
This commit is contained in:
parent
8b094b2057
commit
be1a74b2fd
20 changed files with 1254 additions and 37 deletions
234
pkg/skills/graph.go
Normal file
234
pkg/skills/graph.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
377
pkg/skills/graph_test.go
Normal file
377
pkg/skills/graph_test.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,10 @@ const (
|
||||||
type SkillMetadata struct {
|
type SkillMetadata struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
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 {
|
type SkillInfo struct {
|
||||||
|
|
@ -28,6 +32,8 @@ type SkillInfo struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Tags []string `json:"tags,omitempty"`
|
||||||
|
Domain string `json:"domain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (info SkillInfo) validate() error {
|
func (info SkillInfo) validate() error {
|
||||||
|
|
@ -85,6 +91,8 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
||||||
if metadata != nil {
|
if metadata != nil {
|
||||||
info.Description = metadata.Description
|
info.Description = metadata.Description
|
||||||
info.Name = metadata.Name
|
info.Name = metadata.Name
|
||||||
|
info.Tags = metadata.Tags
|
||||||
|
info.Domain = metadata.Domain
|
||||||
}
|
}
|
||||||
if err := info.validate(); err != nil {
|
if err := info.validate(); err != nil {
|
||||||
slog.Warn("invalid skill from workspace", "name", info.Name, "error", err)
|
slog.Warn("invalid skill from workspace", "name", info.Name, "error", err)
|
||||||
|
|
@ -125,6 +133,8 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
||||||
if metadata != nil {
|
if metadata != nil {
|
||||||
info.Description = metadata.Description
|
info.Description = metadata.Description
|
||||||
info.Name = metadata.Name
|
info.Name = metadata.Name
|
||||||
|
info.Tags = metadata.Tags
|
||||||
|
info.Domain = metadata.Domain
|
||||||
}
|
}
|
||||||
if err := info.validate(); err != nil {
|
if err := info.validate(); err != nil {
|
||||||
slog.Warn("invalid skill from global", "name", info.Name, "error", err)
|
slog.Warn("invalid skill from global", "name", info.Name, "error", err)
|
||||||
|
|
@ -164,6 +174,8 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo {
|
||||||
if metadata != nil {
|
if metadata != nil {
|
||||||
info.Description = metadata.Description
|
info.Description = metadata.Description
|
||||||
info.Name = metadata.Name
|
info.Name = metadata.Name
|
||||||
|
info.Tags = metadata.Tags
|
||||||
|
info.Domain = metadata.Domain
|
||||||
}
|
}
|
||||||
if err := info.validate(); err != nil {
|
if err := info.validate(); err != nil {
|
||||||
slog.Warn("invalid skill from builtin", "name", info.Name, "error", err)
|
slog.Warn("invalid skill from builtin", "name", info.Name, "error", err)
|
||||||
|
|
@ -236,11 +248,17 @@ func (sl *SkillsLoader) BuildSkillsSummary() string {
|
||||||
escapedDesc := escapeXML(s.Description)
|
escapedDesc := escapeXML(s.Description)
|
||||||
escapedPath := escapeXML(s.Path)
|
escapedPath := escapeXML(s.Path)
|
||||||
|
|
||||||
lines = append(lines, fmt.Sprintf(" <skill>"))
|
lines = append(lines, " <skill>")
|
||||||
lines = append(lines, fmt.Sprintf(" <name>%s</name>", escapedName))
|
lines = append(lines, fmt.Sprintf(" <name>%s</name>", escapedName))
|
||||||
lines = append(lines, fmt.Sprintf(" <description>%s</description>", escapedDesc))
|
lines = append(lines, fmt.Sprintf(" <description>%s</description>", escapedDesc))
|
||||||
lines = append(lines, fmt.Sprintf(" <location>%s</location>", escapedPath))
|
lines = append(lines, fmt.Sprintf(" <location>%s</location>", escapedPath))
|
||||||
lines = append(lines, fmt.Sprintf(" <source>%s</source>", s.Source))
|
lines = append(lines, fmt.Sprintf(" <source>%s</source>", s.Source))
|
||||||
|
if len(s.Tags) > 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf(" <tags>%s</tags>", escapeXML(strings.Join(s.Tags, ", "))))
|
||||||
|
}
|
||||||
|
if s.Domain != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf(" <domain>%s</domain>", escapeXML(s.Domain)))
|
||||||
|
}
|
||||||
lines = append(lines, " </skill>")
|
lines = append(lines, " </skill>")
|
||||||
}
|
}
|
||||||
lines = append(lines, "</skills>")
|
lines = append(lines, "</skills>")
|
||||||
|
|
@ -262,23 +280,28 @@ func (sl *SkillsLoader) getSkillMetadata(skillPath string) *SkillMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try JSON first (for backward compatibility)
|
// Try JSON first (for backward compatibility)
|
||||||
var jsonMeta struct {
|
var jsonMeta SkillMetadata
|
||||||
Name string `json:"name"`
|
if err := json.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil && jsonMeta.Name != "" {
|
||||||
Description string `json:"description"`
|
return &jsonMeta
|
||||||
}
|
|
||||||
if err := json.Unmarshal([]byte(frontmatter), &jsonMeta); err == nil {
|
|
||||||
return &SkillMetadata{
|
|
||||||
Name: jsonMeta.Name,
|
|
||||||
Description: jsonMeta.Description,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to simple YAML parsing
|
// Fall back to simple YAML parsing
|
||||||
yamlMeta := sl.parseSimpleYAML(frontmatter)
|
yamlMeta := sl.parseSimpleYAML(frontmatter)
|
||||||
return &SkillMetadata{
|
meta := &SkillMetadata{
|
||||||
Name: yamlMeta["name"],
|
Name: yamlMeta["name"],
|
||||||
Description: yamlMeta["description"],
|
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
|
// parseSimpleYAML parses simple key: value YAML format
|
||||||
|
|
@ -321,6 +344,17 @@ func (sl *SkillsLoader) stripFrontmatter(content string) string {
|
||||||
return re.ReplaceAllString(content, "")
|
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 {
|
func escapeXML(s string) string {
|
||||||
s = strings.ReplaceAll(s, "&", "&")
|
s = strings.ReplaceAll(s, "&", "&")
|
||||||
s = strings.ReplaceAll(s, "<", "<")
|
s = strings.ReplaceAll(s, "<", "<")
|
||||||
|
|
|
||||||
57
pkg/skills/templates.go
Normal file
57
pkg/skills/templates.go
Normal file
|
|
@ -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/<name>/ 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
17
pkg/skills/templates/company/index/SKILL.md
Normal file
17
pkg/skills/templates/company/index/SKILL.md
Normal file
|
|
@ -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
|
||||||
43
pkg/skills/templates/company/onboarding/SKILL.md
Normal file
43
pkg/skills/templates/company/onboarding/SKILL.md
Normal file
|
|
@ -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 <repo-url>
|
||||||
|
cd <repo>
|
||||||
|
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 |
|
||||||
33
pkg/skills/templates/company/org-structure/SKILL.md
Normal file
33
pkg/skills/templates/company/org-structure/SKILL.md
Normal file
|
|
@ -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.
|
||||||
33
pkg/skills/templates/company/processes/SKILL.md
Normal file
33
pkg/skills/templates/company/processes/SKILL.md
Normal file
|
|
@ -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.
|
||||||
31
pkg/skills/templates/company/product-knowledge/SKILL.md
Normal file
31
pkg/skills/templates/company/product-knowledge/SKILL.md
Normal file
|
|
@ -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.
|
||||||
25
pkg/skills/templates/legal/compliance/SKILL.md
Normal file
25
pkg/skills/templates/legal/compliance/SKILL.md
Normal file
|
|
@ -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.
|
||||||
28
pkg/skills/templates/legal/contract-patterns/SKILL.md
Normal file
28
pkg/skills/templates/legal/contract-patterns/SKILL.md
Normal file
|
|
@ -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.
|
||||||
17
pkg/skills/templates/legal/index/SKILL.md
Normal file
17
pkg/skills/templates/legal/index/SKILL.md
Normal file
|
|
@ -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
|
||||||
29
pkg/skills/templates/legal/jurisdiction/SKILL.md
Normal file
29
pkg/skills/templates/legal/jurisdiction/SKILL.md
Normal file
|
|
@ -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.
|
||||||
30
pkg/skills/templates/legal/precedent-chains/SKILL.md
Normal file
30
pkg/skills/templates/legal/precedent-chains/SKILL.md
Normal file
|
|
@ -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.
|
||||||
23
pkg/skills/templates/trading/index/SKILL.md
Normal file
23
pkg/skills/templates/trading/index/SKILL.md
Normal file
|
|
@ -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
|
||||||
32
pkg/skills/templates/trading/market-psychology/SKILL.md
Normal file
32
pkg/skills/templates/trading/market-psychology/SKILL.md
Normal file
|
|
@ -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.
|
||||||
50
pkg/skills/templates/trading/position-sizing/SKILL.md
Normal file
50
pkg/skills/templates/trading/position-sizing/SKILL.md
Normal file
|
|
@ -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.
|
||||||
28
pkg/skills/templates/trading/risk-management/SKILL.md
Normal file
28
pkg/skills/templates/trading/risk-management/SKILL.md
Normal file
|
|
@ -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.
|
||||||
33
pkg/skills/templates/trading/technical-analysis/SKILL.md
Normal file
33
pkg/skills/templates/trading/technical-analysis/SKILL.md
Normal file
|
|
@ -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.
|
||||||
63
pkg/skills/templates_test.go
Normal file
63
pkg/skills/templates_test.go
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue