//go:build ignore // Kit Kit — Meta-agent that builds Kit agents // // A team of domain-specific research experts operate IN PARALLEL to gather // documentation and patterns. The primary agent synthesizes their findings // and WRITES the actual files. // // Each expert runs as a separate `kit` subprocess with a domain-specific // system prompt. Experts are read-only researchers; the primary agent is // the only writer. // // Commands: // // /experts — list available experts and their status // /experts-grid N — set dashboard column count (default 3) // // Usage: kit -e examples/extensions/kit-kit.go package main import ( "bytes" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "sync" "time" "kit/ext" ) // kitJSONOutput matches the JSON envelope produced by `kit --json`. type kitJSONOutput struct { Response string `json:"response"` Model string `json:"model"` Usage *struct { InputTokens int64 `json:"input_tokens"` OutputTokens int64 `json:"output_tokens"` } `json:"usage,omitempty"` } // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- type expertDef struct { Name string Description string Tools string System string // system prompt body File string } type expertState struct { Def expertDef Status string // "idle", "researching", "done", "error" Question string Elapsed time.Duration LastLine string QueryCount int mu sync.Mutex } func (s *expertState) set(status, question, lastLine string, elapsed time.Duration) { s.mu.Lock() defer s.mu.Unlock() if status != "" { s.Status = status } if question != "" { s.Question = question } if lastLine != "" { s.LastLine = lastLine } if elapsed > 0 { s.Elapsed = elapsed } } func (s *expertState) snapshot() (string, string, string, time.Duration, int) { s.mu.Lock() defer s.mu.Unlock() return s.Status, s.Question, s.LastLine, s.Elapsed, s.QueryCount } // --------------------------------------------------------------------------- // Package-level state // --------------------------------------------------------------------------- var ( mu sync.Mutex experts = map[string]*expertState{} gridCols = 3 latestCtx ext.Context hasCtx bool kitBinary string // resolved path to kit executable ) // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func displayName(name string) string { parts := strings.Split(name, "-") for i, w := range parts { if len(w) > 0 { parts[i] = strings.ToUpper(w[:1]) + w[1:] } } return strings.Join(parts, " ") } func runeWidth(s string) int { return len([]rune(s)) } func truncate(s string, max int) string { runes := []rune(s) if len(runes) <= max { return s } if max < 4 { return string(runes[:max]) } return string(runes[:max-3]) + "..." } func pad(s string, width int) string { w := runeWidth(s) if w >= width { return string([]rune(s)[:width]) } return s + strings.Repeat(" ", width-w) } // parseAgentFile reads a .md file with YAML-like frontmatter. // // --- // name: ext-expert // description: Extensions documentation // tools: read,grep,glob // --- // System prompt body here ... func parseAgentFile(path string) *expertDef { raw, err := os.ReadFile(path) if err != nil { return nil } text := string(raw) // Must start with "---\n" if !strings.HasPrefix(text, "---\n") { return nil } rest := text[4:] idx := strings.Index(rest, "\n---\n") if idx < 0 { return nil } frontmatter := rest[:idx] body := strings.TrimSpace(rest[idx+5:]) fm := map[string]string{} for _, line := range strings.Split(frontmatter, "\n") { i := strings.Index(line, ":") if i > 0 { fm[strings.TrimSpace(line[:i])] = strings.TrimSpace(line[i+1:]) } } if fm["name"] == "" { return nil } return &expertDef{ Name: fm["name"], Description: fm["description"], Tools: fm["tools"], System: body, File: path, } } func loadExperts(cwd string) { mu.Lock() defer mu.Unlock() experts = map[string]*expertState{} dir := filepath.Join(cwd, ".kit", "agents", "kit-kit") entries, err := os.ReadDir(dir) if err != nil { return } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { continue } if e.Name() == "orchestrator.md" { continue } def := parseAgentFile(filepath.Join(dir, e.Name())) if def == nil { continue } key := strings.ToLower(def.Name) experts[key] = &expertState{ Def: *def, Status: "idle", } } } func expertList() []*expertState { mu.Lock() defer mu.Unlock() list := make([]*expertState, 0, len(experts)) for _, s := range experts { list = append(list, s) } return list } func expertNames() string { list := expertList() names := make([]string, len(list)) for i, s := range list { names[i] = displayName(s.Def.Name) } return strings.Join(names, ", ") } // --------------------------------------------------------------------------- // Widget grid rendering // --------------------------------------------------------------------------- func renderCard(s *expertState, w int) []string { status, question, lastLine, elapsed, queryCount := s.snapshot() inner := w - 2 // inside the box-drawing borders // Name line name := truncate(displayName(s.Def.Name), inner-1) // Status line var icon string switch status { case "idle": icon = "○" case "researching": icon = "◉" case "done": icon = "✓" default: icon = "✗" } statusText := icon + " " + status if status != "idle" { statusText += fmt.Sprintf(" %ds", int(elapsed.Seconds())) } if queryCount > 0 { statusText += fmt.Sprintf(" (%d)", queryCount) } statusText = truncate(statusText, inner-1) // Work line (question or description) work := question if work == "" { work = s.Def.Description } work = truncate(work, inner-1) // Last output line last := lastLine if last == "" { last = "—" } last = truncate(last, inner-1) // Build card (use rune width for box-drawing alignment) topBar := "─ " + name + " " if runeWidth(topBar) < inner { topBar += strings.Repeat("─", inner-runeWidth(topBar)) } return []string{ "┌" + truncate(topBar, inner) + "┐", "│ " + pad(statusText, inner-1) + "│", "│ " + pad(work, inner-1) + "│", "│ " + pad(last, inner-1) + "│", "└" + strings.Repeat("─", inner) + "┘", } } func buildGrid() string { list := expertList() if len(list) == 0 { return "No experts found. Add agent .md files to .kit/agents/kit-kit/" } cols := gridCols if cols > len(list) { cols = len(list) } // Card width: aim for ~28 chars per card cardWidth := 28 gap := 1 var lines []string for i := 0; i < len(list); i += cols { end := i + cols if end > len(list) { end = len(list) } row := list[i:end] // Render each card in this row cards := make([][]string, len(row)) maxHeight := 0 for j, s := range row { cards[j] = renderCard(s, cardWidth) if len(cards[j]) > maxHeight { maxHeight = len(cards[j]) } } // Merge columns line by line for line := 0; line < maxHeight; line++ { var parts []string for _, card := range cards { if line < len(card) { parts = append(parts, card[line]) } else { parts = append(parts, strings.Repeat(" ", cardWidth)) } } lines = append(lines, strings.Join(parts, strings.Repeat(" ", gap))) } } return strings.Join(lines, "\n") } func updateWidget() { mu.Lock() ctx := latestCtx ok := hasCtx mu.Unlock() if !ok { return } ctx.SetWidget(ext.WidgetConfig{ ID: "kit-kit:grid", Placement: ext.WidgetAbove, Content: ext.WidgetContent{ Text: buildGrid(), }, Style: ext.WidgetStyle{ NoBorder: true, BorderColor: "", }, Priority: 10, }) } func updateFooter() { mu.Lock() ctx := latestCtx ok := hasCtx mu.Unlock() if !ok { return } list := expertList() active := 0 done := 0 for _, s := range list { st, _, _, _, _ := s.snapshot() switch st { case "researching": active++ case "done": done++ } } var mid string if active > 0 { mid = fmt.Sprintf(" ◉ %d researching", active) } else if done > 0 { mid = fmt.Sprintf(" ✓ %d done", done) } text := fmt.Sprintf("%s | Kit Kit%s", ctx.Model, mid) ctx.SetFooter(ext.HeaderFooterConfig{ Content: ext.WidgetContent{Text: text}, Style: ext.WidgetStyle{BorderColor: "#89b4fa"}, }) } // --------------------------------------------------------------------------- // Kit binary resolution // --------------------------------------------------------------------------- func findKitBinary() string { // Try the current process executable first. if exe, err := os.Executable(); err == nil { if _, err := os.Stat(exe); err == nil { return exe } } // Fall back to PATH lookup. if p, err := exec.LookPath("kit"); err == nil { return p } return "kit" } // --------------------------------------------------------------------------- // Expert query (subprocess) // --------------------------------------------------------------------------- func queryExpert(name, question string) (output string, exitCode int, elapsed time.Duration) { mu.Lock() state, ok := experts[strings.ToLower(name)] mu.Unlock() if !ok { return fmt.Sprintf("Expert %q not found.", name), 1, 0 } // Mark as researching. state.mu.Lock() if state.Status == "researching" { state.mu.Unlock() return fmt.Sprintf("Expert %q is already researching.", displayName(name)), 1, 0 } state.Status = "researching" state.Question = question state.Elapsed = 0 state.LastLine = "" state.QueryCount++ state.mu.Unlock() updateWidget() start := time.Now() // Timer goroutine: update widget every second while researching. done := make(chan struct{}) go func() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-done: return case <-ticker.C: state.set("", "", "", time.Since(start)) updateWidget() updateFooter() } } }() // Write system prompt to temp file. tmpFile, err := os.CreateTemp("", "kit-kit-*.txt") if err != nil { close(done) state.set("error", "", "temp file error: "+err.Error(), time.Since(start)) updateWidget() updateFooter() return "Error creating temp file: " + err.Error(), 1, time.Since(start) } defer os.Remove(tmpFile.Name()) if _, err := tmpFile.WriteString(state.Def.System); err != nil { tmpFile.Close() close(done) state.set("error", "", "write error: "+err.Error(), time.Since(start)) updateWidget() updateFooter() return "Error writing system prompt: " + err.Error(), 1, time.Since(start) } tmpFile.Close() // Build subprocess arguments. Use --json for structured output parsing. // Don't pass --model; the subprocess inherits the same config/env default. args := []string{ "--json", "--no-session", "--no-extensions", "--system-prompt", tmpFile.Name(), question, } var stdoutBuf, stderrBuf bytes.Buffer cmd := exec.Command(kitBinary, args...) cmd.Env = os.Environ() cmd.Stdout = &stdoutBuf cmd.Stderr = &stderrBuf err = cmd.Run() close(done) elapsed = time.Since(start) if err != nil { // On error, prefer stderr for the error message; fall back to stdout. errText := strings.TrimSpace(stderrBuf.String()) if errText == "" { errText = strings.TrimSpace(stdoutBuf.String()) } errLine := errText if idx := strings.Index(errLine, "\n"); idx >= 0 { errLine = errLine[:idx] } state.set("error", "", truncate(strings.TrimSpace(errLine), 80), elapsed) updateWidget() updateFooter() code := 1 if exitErr, ok := err.(*exec.ExitError); ok { code = exitErr.ExitCode() } return errText, code, elapsed } // Parse JSON output from subprocess. var parsed kitJSONOutput result := strings.TrimSpace(stdoutBuf.String()) if err := json.Unmarshal([]byte(result), &parsed); err == nil { result = parsed.Response } // else: fall back to raw stdout (e.g. older kit binary without --json) // Extract last non-empty line for the card. lines := strings.Split(result, "\n") var lastLine string for i := len(lines) - 1; i >= 0; i-- { if strings.TrimSpace(lines[i]) != "" { lastLine = lines[i] break } } state.set("done", "", truncate(lastLine, 60), elapsed) updateWidget() updateFooter() return result, 0, elapsed } // --------------------------------------------------------------------------- // Orchestrator system prompt // --------------------------------------------------------------------------- func buildOrchestratorPrompt(cwd string) string { orchPath := filepath.Join(cwd, ".kit", "agents", "kit-kit", "orchestrator.md") raw, err := os.ReadFile(orchPath) if err != nil { // Fallback: generate a basic orchestrator prompt. return buildDefaultOrchestratorPrompt() } text := string(raw) // Strip frontmatter if present. if strings.HasPrefix(text, "---\n") { if idx := strings.Index(text[4:], "\n---\n"); idx >= 0 { text = strings.TrimSpace(text[4+idx+5:]) } } list := expertList() catalog := buildExpertCatalog(list) names := make([]string, len(list)) for i, s := range list { names[i] = displayName(s.Def.Name) } text = strings.ReplaceAll(text, "{{EXPERT_COUNT}}", fmt.Sprintf("%d", len(list))) text = strings.ReplaceAll(text, "{{EXPERT_NAMES}}", strings.Join(names, ", ")) text = strings.ReplaceAll(text, "{{EXPERT_CATALOG}}", catalog) return text } func buildExpertCatalog(list []*expertState) string { var sb strings.Builder for _, s := range list { fmt.Fprintf(&sb, "### %s\n", displayName(s.Def.Name)) fmt.Fprintf(&sb, "**Query as:** `%s`\n", s.Def.Name) fmt.Fprintf(&sb, "%s\n\n", s.Def.Description) } return sb.String() } func buildDefaultOrchestratorPrompt() string { list := expertList() names := make([]string, len(list)) for i, s := range list { names[i] = displayName(s.Def.Name) } catalog := buildExpertCatalog(list) return fmt.Sprintf(`You are Kit Kit, an orchestrator agent with %d domain experts: %s. Use the query_experts tool to consult experts IN PARALLEL before writing code. Always query multiple experts at once when the task spans multiple domains. ## Available Experts %s ## Workflow 1. Analyze the user's request to identify which domains are relevant. 2. Use query_experts to ask specific questions of the relevant experts. 3. Synthesize the expert findings into a coherent implementation. 4. Write the actual code/files — you are the only agent that writes. ## Rules - ALWAYS query experts before implementing. Never guess. - Ask SPECIFIC questions. "How does X work?" is better than "Tell me about X". - Query multiple experts in a single call when possible (they run in parallel). - If an expert returns insufficient info, query again with a more specific question. `, len(list), strings.Join(names, ", "), catalog) } // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- func Init(api ext.API) { kitBinary = findKitBinary() // ── Session Start: load experts, show grid ── api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) { mu.Lock() latestCtx = ctx hasCtx = true mu.Unlock() loadExperts(ctx.CWD) updateWidget() updateFooter() names := expertNames() n := len(expertList()) if n > 0 { ctx.PrintInfo(fmt.Sprintf( "Kit Kit loaded — %d experts: %s\n\n"+ "/experts List experts and status\n"+ "/experts-grid N Set grid columns (1-5)\n\n"+ "Ask me to build any Kit component!", n, names)) } else { ctx.PrintInfo( "Kit Kit loaded — no experts found.\n\n" + "Add agent .md files to .kit/agents/kit-kit/ to get started.\n" + "See examples/extensions/kit-kit-agents/ for samples.") } }) // ── Before Agent Start: inject orchestrator system prompt ── api.OnBeforeAgentStart(func(_ ext.BeforeAgentStartEvent, ctx ext.Context) *ext.BeforeAgentStartResult { mu.Lock() latestCtx = ctx mu.Unlock() prompt := buildOrchestratorPrompt(ctx.CWD) return &ext.BeforeAgentStartResult{SystemPrompt: &prompt} }) // ── Agent End: update footer ── api.OnAgentEnd(func(_ ext.AgentEndEvent, ctx ext.Context) { mu.Lock() latestCtx = ctx mu.Unlock() updateFooter() }) // ── Session Shutdown: cleanup ── api.OnSessionShutdown(func(_ ext.SessionShutdownEvent, ctx ext.Context) { ctx.RemoveWidget("kit-kit:grid") ctx.RemoveFooter() }) // ── Tool: query_experts ── api.RegisterTool(ext.ToolDef{ Name: "query_experts", Description: `Query one or more Kit domain experts IN PARALLEL. All experts run simultaneously as concurrent subprocesses. Pass an array of queries — each with an expert name and a specific question. All experts start at the same time and their results are returned together. Available experts are loaded from .kit/agents/kit-kit/*.md at session start. The default set includes: - ext-expert: Kit extensions — tools, events, commands, widgets, editor interceptors - tui-expert: Kit TUI — Bubble Tea v2 components, rendering, theming, layout - llm-expert: Kit LLM system — providers, streaming, agent loop, tool execution Ask specific questions about what you need to BUILD. Each expert will return documentation excerpts, code patterns, and implementation guidance.`, Parameters: `{ "type": "object", "properties": { "queries": { "type": "array", "description": "Array of expert queries to run in parallel", "items": { "type": "object", "properties": { "expert": { "type": "string", "description": "Expert name (e.g. ext-expert, tui-expert, llm-expert)" }, "question": { "type": "string", "description": "Specific question about what you need to build" } }, "required": ["expert", "question"] } } }, "required": ["queries"] }`, Execute: func(input string) (string, error) { var params struct { Queries []struct { Expert string `json:"expert"` Question string `json:"question"` } `json:"queries"` } if err := json.Unmarshal([]byte(input), ¶ms); err != nil { return "", fmt.Errorf("invalid parameters: %w", err) } if len(params.Queries) == 0 { return "No queries provided.", nil } // Launch all experts in parallel. type result struct { Expert string Question string Output string ExitCode int Elapsed time.Duration } results := make([]result, len(params.Queries)) var wg sync.WaitGroup for i, q := range params.Queries { wg.Add(1) go func(idx int, expert, question string) { defer wg.Done() out, code, elapsed := queryExpert(expert, question) results[idx] = result{ Expert: expert, Question: question, Output: out, ExitCode: code, Elapsed: elapsed, } }(i, q.Expert, q.Question) } wg.Wait() // Build combined response. var sb strings.Builder for _, r := range results { icon := "✓" if r.ExitCode != 0 { icon = "✗" } fmt.Fprintf(&sb, "## [%s] %s (%ds)\n\n", icon, displayName(r.Expert), int(r.Elapsed.Seconds())) out := r.Output if len(out) > 12000 { out = out[:12000] + "\n\n... [truncated — ask follow-up for more]" } sb.WriteString(out) sb.WriteString("\n\n---\n\n") } return sb.String(), nil }, }) // ── Tool Renderer: query_experts ── api.RegisterToolRenderer(ext.ToolRenderConfig{ ToolName: "query_experts", DisplayName: "Query Experts", BorderColor: "#89b4fa", RenderHeader: func(toolArgs string, width int) string { var args struct { Queries []struct { Expert string `json:"expert"` } `json:"queries"` } if err := json.Unmarshal([]byte(toolArgs), &args); err != nil { return "" } names := make([]string, len(args.Queries)) for i, q := range args.Queries { names[i] = displayName(q.Expert) } header := fmt.Sprintf("%d experts in parallel: %s", len(args.Queries), strings.Join(names, ", ")) return truncate(header, width) }, RenderBody: func(toolResult string, isError bool, width int) string { if isError { return "" // fall back to default } // Show compact summary: extract ## headers with status var lines []string for _, line := range strings.Split(toolResult, "\n") { if strings.HasPrefix(line, "## [") { lines = append(lines, line[3:]) // strip "## " } } if len(lines) == 0 { return "" } return strings.Join(lines, " · ") }, }) // ── Command: /experts ── api.RegisterCommand(ext.CommandDef{ Name: "experts", Description: "List available Kit Kit experts and their status", Execute: func(args string, ctx ext.Context) (string, error) { mu.Lock() latestCtx = ctx mu.Unlock() list := expertList() if len(list) == 0 { return "No experts loaded. Add agent .md files to .kit/agents/kit-kit/", nil } var sb strings.Builder for _, s := range list { status, _, _, _, qc := s.snapshot() fmt.Fprintf(&sb, "%s (%s, queries: %d): %s\n", displayName(s.Def.Name), status, qc, s.Def.Description) } return sb.String(), nil }, }) // ── Command: /experts-grid ── api.RegisterCommand(ext.CommandDef{ Name: "experts-grid", Description: "Set expert grid columns: /experts-grid <1-5>", Execute: func(args string, ctx ext.Context) (string, error) { mu.Lock() latestCtx = ctx mu.Unlock() args = strings.TrimSpace(args) n := 0 if _, err := fmt.Sscanf(args, "%d", &n); err != nil || n < 1 || n > 5 { return "Usage: /experts-grid <1-5>", nil } mu.Lock() gridCols = n mu.Unlock() updateWidget() return fmt.Sprintf("Grid set to %d columns.", n), nil }, }) }