loop: add knowledge_base tool for on-demand information

The knowledge_base tool provides a way for agents to access specialized information
when needed. Initial topics include:

- sketch: how to use Sketch, including SSH, secrets, and file management
- go_iterators: information about Go's iterator feature added in Go 1.22

Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/claudetool/knowledge_base.go b/claudetool/knowledge_base.go
new file mode 100644
index 0000000..21034ee
--- /dev/null
+++ b/claudetool/knowledge_base.go
@@ -0,0 +1,110 @@
+package claudetool
+
+import (
+	"context"
+	_ "embed"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"strings"
+	"text/template"
+
+	"sketch.dev/llm"
+	"sketch.dev/llm/conversation"
+)
+
+// KnowledgeBase provides on-demand specialized knowledge to the agent.
+var KnowledgeBase = &llm.Tool{
+	Name:        kbName,
+	Description: kbDescription,
+	InputSchema: llm.MustSchema(kbInputSchema),
+	Run:         kbRun,
+}
+
+// TODO: BYO knowledge bases? could do that for strings.Lines, for example.
+// TODO: support Q&A mode instead of reading full text in?
+
+const (
+	kbName        = "knowledge_base"
+	kbDescription = `Retrieve specialized information that you need but don't have in your context.
+
+When to use this tool:
+
+For the "sketch" topic:
+- The user is asking how to USE Sketch itself (not asking Sketch to perform a task)
+- The user has questions about Sketch functionality, setup, or capabilities
+- The user needs help with Sketch-specific concepts like running commands, secrets management, git integration
+- The query is about "How do I do X in Sketch?" or "Is it possible to Y in Sketch?" or just "Help"
+- The user is confused about how a Sketch feature works or how to access it
+
+For the "strings_lines" topic:
+- Any mentions of strings.Lines in the code, by the codereview, or by the user
+- When implementing code that iterates over lines in a Go string
+
+Available topics:
+- sketch: documentation on Sketch usage
+- strings_lines: details about the Go strings.Lines API
+`
+
+	kbInputSchema = `
+{
+  "type": "object",
+  "required": ["topic"],
+  "properties": {
+    "topic": {
+      "type": "string",
+      "description": "Topic to retrieve information about",
+      "enum": ["sketch", "strings_lines"]
+    }
+  }
+}
+`
+)
+
+type kbInput struct {
+	Topic string `json:"topic"`
+}
+
+//go:embed kb/sketch.txt
+var sketchContent string
+
+//go:embed kb/strings_lines.txt
+var stringsLinesContent string
+
+var sketchTemplate = template.Must(template.New("sketch").Parse(sketchContent))
+
+func kbRun(ctx context.Context, m json.RawMessage) ([]llm.Content, error) {
+	var input kbInput
+	if err := json.Unmarshal(m, &input); err != nil {
+		return nil, err
+	}
+
+	// Sanitize topic name (simple lowercase conversion for now)
+	topic := strings.ToLower(strings.TrimSpace(input.Topic))
+	slog.InfoContext(ctx, "knowledge base request", "topic", topic)
+
+	// Process content based on topic
+	switch input.Topic {
+	case "sketch":
+		info := conversation.ToolCallInfoFromContext(ctx)
+		sessionID, _ := info.Convo.ExtraData["session_id"].(string)
+		branch, _ := info.Convo.ExtraData["branch"].(string)
+		dot := struct {
+			SessionID string
+			Branch    string
+		}{
+			SessionID: sessionID,
+			Branch:    branch,
+		}
+		buf := new(strings.Builder)
+		if err := sketchTemplate.Execute(buf, dot); err != nil {
+			return nil, fmt.Errorf("template execution error: %w", err)
+		}
+		return llm.TextContent(buf.String()), nil
+	case "strings_lines":
+		// No special processing for other topics
+		return llm.TextContent(stringsLinesContent), nil
+	default:
+		return nil, fmt.Errorf("unknown topic: %s", input.Topic)
+	}
+}