diff --git a/README.md b/README.md index 742bd72..2442836 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ Entries are stored in `/.agents/.yaml`: - `conventions.yaml` — patterns, naming, build commands - `subsystems.yaml` — how things work - `decisions.yaml` — why choices were made +- `workflows.yaml` — procedures the agent runs on demand (brainstorm, plan, debug, verify) - `_meta.yaml` — project metadata Category files use a `project:`/`version:`/`entries:` wrapper — a bare YAML diff --git a/examples/KNOWLEDGE.md b/examples/KNOWLEDGE.md index 792eeda..b9f1439 100644 --- a/examples/KNOWLEDGE.md +++ b/examples/KNOWLEDGE.md @@ -8,9 +8,11 @@ Query knowledge for current context: - `query_knowledge(project="", category="conventions")` — patterns, build, naming - `query_knowledge(project="", query="")` — how it works - `query_knowledge(project="", category="decisions")` — past choices +- `query_knowledge(project="", category="workflows")` — procedures to run ## After learning something significant Write it back: - `write_knowledge(project="", category="subsystems", summary="...", detail="...", source="...")` - `write_knowledge(project="", category="decisions", summary="...", detail="...", source="...")` +- `write_knowledge(project="", category="workflows", summary="...", detail="...", source="...")` diff --git a/install/skill.go b/install/skill.go index 38744f1..1c01f69 100644 --- a/install/skill.go +++ b/install/skill.go @@ -14,7 +14,7 @@ func RenderSkillMD(registry []tools.ToolSpec) []byte { } md := "---\n" + "name: knowledge-mcp\n" + - "description: \"Use the persistent knowledge store for project conventions, subsystems, and decisions. Triggers on: what are our conventions, does this project have rules about, how does this subsystem work, why was this decision made, capture this decision, write that down as knowledge, update that entry, knowledge store, .agents, list_knowledge, query_knowledge, write_knowledge, update_knowledge, init_knowledge.\"\n" + + "description: \"Use the persistent knowledge store for project conventions, subsystems, decisions, and workflows. Triggers on: what are our conventions, does this project have rules about, how does this subsystem work, why was this decision made, run the brainstorm workflow, run the plan workflow, debug workflow, verify workflow, capture this decision, write that down as knowledge, update that entry, knowledge store, .agents, list_knowledge, query_knowledge, write_knowledge, update_knowledge, init_knowledge.\"\n" + "---\n" + "\n" + "# Knowledge Store (knowledge-mcp)\n" + diff --git a/knowledge/schema.go b/knowledge/schema.go index 5f9ac08..29c1d7d 100644 --- a/knowledge/schema.go +++ b/knowledge/schema.go @@ -38,7 +38,7 @@ func Today() string { // ValidCategories returns the set of valid category names. func ValidCategories() []string { - return []string{"conventions", "subsystems", "decisions"} + return []string{"conventions", "subsystems", "decisions", "workflows"} } // CategoryPrefix maps category names to their ID prefixes. @@ -50,6 +50,8 @@ func CategoryPrefix(category string) string { return "sub" case "decisions": return "dec" + case "workflows": + return "wf" default: return "" } diff --git a/knowledge/store_test.go b/knowledge/store_test.go index 5e0b0da..17ad76e 100644 --- a/knowledge/store_test.go +++ b/knowledge/store_test.go @@ -130,12 +130,23 @@ func TestValidationHelpers(t *testing.T) { if !IsValidCategory("conventions") { t.Error("conventions should be valid") } + if !IsValidCategory("workflows") { + t.Error("workflows should be valid") + } if IsValidCategory("invalid") { t.Error("invalid should not be valid") } if CategoryPrefix("conventions") != "conv" { t.Errorf("prefix = %q, want %q", CategoryPrefix("conventions"), "conv") } + if CategoryPrefix("workflows") != "wf" { + t.Errorf("prefix = %q, want %q", CategoryPrefix("workflows"), "wf") + } + for _, cat := range ValidCategories() { + if CategoryPrefix(cat) == "" { + t.Errorf("category %q has no ID prefix", cat) + } + } } func TestEnsureDir(t *testing.T) { diff --git a/tools/list.go b/tools/list.go index da4a2fb..8cc2a2d 100644 --- a/tools/list.go +++ b/tools/list.go @@ -26,7 +26,8 @@ func ListHandler(res *projects.Resolver, idx *search.Index) func(context.Context filterCategory := request.GetString("category", "") if filterCategory != "" && !knowledge.IsValidCategory(filterCategory) { - return mcp.NewToolResultError(fmt.Sprintf("invalid category: %q (must be conventions, subsystems, or decisions)", filterCategory)), nil + return mcp.NewToolResultError(fmt.Sprintf( + "invalid category: %q (must be %s)", filterCategory, strings.Join(knowledge.ValidCategories(), ", "))), nil } output := "" diff --git a/tools/query.go b/tools/query.go index d6f0758..a172b71 100644 --- a/tools/query.go +++ b/tools/query.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/renderorange/knowledge-mcp/knowledge" @@ -29,7 +30,8 @@ func QueryHandler(res *projects.Resolver, idx *search.Index) func(context.Contex limit := int(request.GetFloat("limit", 10)) if category != "" && !knowledge.IsValidCategory(category) { - return mcp.NewToolResultError(fmt.Sprintf("invalid category: %q (must be conventions, subsystems, or decisions)", category)), nil + return mcp.NewToolResultError(fmt.Sprintf( + "invalid category: %q (must be %s)", category, strings.Join(knowledge.ValidCategories(), ", "))), nil } results, err := idx.Query(ref.Address, query, category, limit) diff --git a/tools/registry.go b/tools/registry.go index 11c8393..5946d92 100644 --- a/tools/registry.go +++ b/tools/registry.go @@ -1,6 +1,17 @@ package tools -import "github.com/mark3labs/mcp-go/mcp" +import ( + "fmt" + "strings" + + "github.com/mark3labs/mcp-go/mcp" + "github.com/renderorange/knowledge-mcp/knowledge" +) + +// categoryNames keeps tool descriptions in sync with ValidCategories. +func categoryNames() string { + return strings.Join(knowledge.ValidCategories(), ", ") +} // Tool name constants keep main.go, the registry, and generated // instructions in sync. @@ -43,7 +54,7 @@ var Registry = []ToolSpec{ Description: "Add a new knowledge entry to a project's .agents/ store", Params: []ParamSpec{ {Name: "project", Description: "Project name (bare if unique, else root/project)", Type: "string", Required: true}, - {Name: "category", Description: "Category: conventions, subsystems, or decisions", Type: "string", Required: true}, + {Name: "category", Description: fmt.Sprintf("Category: %s", categoryNames()), Type: "string", Required: true}, {Name: "summary", Description: "One-line description (max 100 chars)", Type: "string", Required: true}, {Name: "detail", Description: "Concise knowledge (10-20 lines). Summarize key facts, don't copy source files. Include only query-able information.", Type: "string", Required: true}, {Name: "source", Description: "How this was learned (provenance)", Type: "string", Required: true}, @@ -56,7 +67,7 @@ var Registry = []ToolSpec{ Params: []ParamSpec{ {Name: "project", Description: "Project name (bare if unique, else root/project)", Type: "string", Required: true}, {Name: "query", Description: "Full-text search query", Type: "string"}, - {Name: "category", Description: "Filter by category: conventions, subsystems, or decisions", Type: "string"}, + {Name: "category", Description: fmt.Sprintf("Filter by category: %s", categoryNames()), Type: "string"}, {Name: "limit", Description: "Max results (default 10)", Type: "number"}, }, }, @@ -65,7 +76,7 @@ var Registry = []ToolSpec{ Description: "List all knowledge entries for a project. Entries with a rule are shown first under constraints.", Params: []ParamSpec{ {Name: "project", Description: "Project name (bare if unique, else root/project)", Type: "string", Required: true}, - {Name: "category", Description: "Filter by category: conventions, subsystems, or decisions", Type: "string"}, + {Name: "category", Description: fmt.Sprintf("Filter by category: %s", categoryNames()), Type: "string"}, }, }, { @@ -73,7 +84,7 @@ var Registry = []ToolSpec{ Description: "Update an existing knowledge entry by ID", Params: []ParamSpec{ {Name: "project", Description: "Project name (bare if unique, else root/project)", Type: "string", Required: true}, - {Name: "category", Description: "Category: conventions, subsystems, or decisions", Type: "string", Required: true}, + {Name: "category", Description: fmt.Sprintf("Category: %s", categoryNames()), Type: "string", Required: true}, {Name: "id", Description: "Entry ID to update (e.g., conv-001)", Type: "string", Required: true}, {Name: "summary", Description: "New summary (optional)", Type: "string"}, {Name: "detail", Description: "New detail (optional)", Type: "string"}, diff --git a/tools/tools_test.go b/tools/tools_test.go index fa8c3fd..9e1f60b 100644 --- a/tools/tools_test.go +++ b/tools/tools_test.go @@ -130,6 +130,58 @@ func TestWriteHandler(t *testing.T) { } } +func TestWriteHandlerWorkflowsCategory(t *testing.T) { + root := t.TempDir() + dir := filepath.Join(root, "test") + agentsDir := filepath.Join(dir, ".agents") + knowledge.EnsureDir(agentsDir) + + idx, _ := search.NewIndex() + defer idx.Close() + + resolver, _, err := projects.Build([]string{root}, nil, "") + if err != nil { + t.Fatalf("projects.Build() error: %v", err) + } + + handler := WriteHandler(resolver, idx) + + req := mcp.CallToolRequest{} + req.Params.Arguments = map[string]interface{}{ + "project": "test", + "category": "workflows", + "summary": "Brainstorm before building", + "detail": "Ask one question at a time and play back what you heard.", + "source": "test", + "rule": "Do not build until the user approves the description", + } + + result, err := handler(context.Background(), req) + if err != nil { + t.Fatalf("WriteHandler() error: %v", err) + } + if result == nil { + t.Fatal("WriteHandler() returned nil") + } + if result.IsError { + t.Fatal("WriteHandler() returned an error result for the workflows category") + } + + loaded, loadErr := knowledge.Load(knowledge.CategoryFilePath(agentsDir, "workflows")) + if loadErr != nil { + t.Fatalf("Load() error: %v", loadErr) + } + if len(loaded.Entries) != 1 { + t.Fatalf("len(Entries) = %d, want 1", len(loaded.Entries)) + } + if loaded.Entries[0].ID != "wf-001" { + t.Errorf("ID = %q, want %q", loaded.Entries[0].ID, "wf-001") + } + if loaded.Entries[0].Rule == "" { + t.Error("Rule should not be empty") + } +} + func TestWriteHandlerValidation(t *testing.T) { root := t.TempDir() dir := filepath.Join(root, "test") diff --git a/tools/update.go b/tools/update.go index 1a04607..10631f5 100644 --- a/tools/update.go +++ b/tools/update.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/renderorange/knowledge-mcp/knowledge" @@ -24,7 +25,8 @@ func UpdateHandler(res *projects.Resolver, idx *search.Index) func(context.Conte return mcp.NewToolResultError(err.Error()), nil } if !knowledge.IsValidCategory(category) { - return mcp.NewToolResultError(fmt.Sprintf("invalid category: %q (must be conventions, subsystems, or decisions)", category)), nil + return mcp.NewToolResultError(fmt.Sprintf( + "invalid category: %q (must be %s)", category, strings.Join(knowledge.ValidCategories(), ", "))), nil } id, err := request.RequireString("id") diff --git a/tools/write.go b/tools/write.go index 695bbb8..51c6ea6 100644 --- a/tools/write.go +++ b/tools/write.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "github.com/mark3labs/mcp-go/mcp" "github.com/renderorange/knowledge-mcp/knowledge" @@ -28,7 +29,8 @@ func WriteHandler(res *projects.Resolver, idx *search.Index) func(context.Contex return mcp.NewToolResultError(err.Error()), nil } if !knowledge.IsValidCategory(category) { - return mcp.NewToolResultError(fmt.Sprintf("invalid category: %q (must be conventions, subsystems, or decisions)", category)), nil + return mcp.NewToolResultError(fmt.Sprintf( + "invalid category: %q (must be %s)", category, strings.Join(knowledge.ValidCategories(), ", "))), nil } summary, err := request.RequireString("summary")