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:
ZanzyTHEbar 2026-02-18 15:53:54 +00:00
parent 8b094b2057
commit be1a74b2fd
20 changed files with 1254 additions and 37 deletions

234
pkg/skills/graph.go Normal file
View 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
View 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
}

View file

@ -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(" <skill>"))
lines = append(lines, " <skill>")
lines = append(lines, fmt.Sprintf(" <name>%s</name>", escapedName))
lines = append(lines, fmt.Sprintf(" <description>%s</description>", escapedDesc))
lines = append(lines, fmt.Sprintf(" <location>%s</location>", escapedPath))
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, "</skills>")
@ -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, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")

57
pkg/skills/templates.go Normal file
View 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)
})
}

View 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

View 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 |

View 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.

View 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.

View 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.

View 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.

View 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.

View 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

View 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.

View 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.

View 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

View 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.

View 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.

View 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.

View 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.

View 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")
}