# Hyperguild Phase 1 — Foundation Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add brain access, session logging, tier detection, and a retrospective worker to the supervisor MCP server, turning it into the foundation of the hyperguild SDO. **Architecture:** The supervisor repo grows two new subdirectories: `ingestion/` (a separate Go HTTP server that wraps brain file I/O and text search) and `brain/` (the wiki content + session logs). The supervisor MCP server gains four new tool groups — brain, org, sessionlog, retrospective — that call the ingestion server internally or append to local JSONL files. The existing TDD skill handlers are updated to automatically write session log entries after each invocation. **Tech Stack:** Go 1.26, net/http (stdlib only), testify, JSONL for session logs, plain text search for brain queries (no Qdrant in Phase 1). --- ## File Map **New — ingestion module:** - `ingestion/go.mod` — separate Go module `github.com/mathiasbq/hyperguild/ingestion` - `ingestion/cmd/server/main.go` — HTTP server entry point (:3300) - `ingestion/internal/api/handler.go` — `/query` and `/write` handlers - `ingestion/internal/api/handler_test.go` - `ingestion/internal/search/search.go` — full-text search across wiki files - `ingestion/internal/search/search_test.go` **New — supervisor packages:** - `internal/tier/tier.go` — tier detection by probing endpoints - `internal/tier/tier_test.go` - `internal/session/session.go` — append/read JSONL session logs - `internal/session/session_test.go` - `internal/skills/brain/skill.go` — brain_query + brain_write MCP tools - `internal/skills/brain/handlers.go` - `internal/skills/brain/handlers_test.go` - `internal/skills/org/skill.go` — tier MCP tool - `internal/skills/org/handlers.go` - `internal/skills/org/handlers_test.go` - `internal/skills/sessionlog/skill.go` — session_log MCP tool - `internal/skills/sessionlog/handlers.go` - `internal/skills/sessionlog/handlers_test.go` - `internal/skills/retrospective/skill.go` — retrospective MCP tool - `internal/skills/retrospective/handlers.go` - `internal/skills/retrospective/handlers_test.go` **New — config files:** - `config/supervisor/protocols.md` - `config/supervisor/retrospective.md` - `brain/wiki/concepts/.gitkeep` - `brain/wiki/entities/.gitkeep` - `brain/wiki/sources/.gitkeep` - `brain/raw/.gitkeep` - `brain/sessions/.gitkeep` - `brain/training-data/sft/.gitkeep` - `brain/training-data/dpo/.gitkeep` - `brain/training-data/rl/.gitkeep` **Modified:** - `internal/skills/tdd/handlers.go` — call session_log after each phase - `internal/config/config.go` — add IngestBaseURL, SessionsDir, BrainDir - `cmd/supervisor/main.go` — wire new skills - `config/models.yaml` — add retrospective model - `Taskfile.yml` — add ingestion server tasks - `.context/mcp.json` — update server list - `.env.example` — add new vars --- ## Task 1: ingestion/ module scaffold **Files:** - Create: `ingestion/go.mod` - Create: `ingestion/cmd/server/main.go` - [ ] **Step 1: Create the ingestion go.mod** ``` module github.com/mathiasbq/hyperguild/ingestion go 1.26.1 require github.com/stretchr/testify v1.11.1 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ``` Save as `ingestion/go.mod`. Then run: ```bash cd ingestion && go mod tidy ``` - [ ] **Step 2: Create the server entry point** ```go // ingestion/cmd/server/main.go package main import ( "log/slog" "net/http" "os" "github.com/mathiasbq/hyperguild/ingestion/internal/api" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) brainDir := os.Getenv("INGEST_BRAIN_DIR") if brainDir == "" { brainDir = "../brain" } port := os.Getenv("INGEST_PORT") if port == "" { port = "3300" } h := api.NewHandler(brainDir, logger) mux := http.NewServeMux() mux.HandleFunc("/query", h.Query) mux.HandleFunc("/write", h.Write) addr := ":" + port logger.Info("ingestion server starting", "addr", addr, "brain_dir", brainDir) if err := http.ListenAndServe(addr, mux); err != nil { logger.Error("server stopped", "err", err) os.Exit(1) } } ``` - [ ] **Step 3: Verify it compiles (handler not yet written — expect error)** ```bash cd ingestion && go build ./... 2>&1 ``` Expected: error about missing `api` package. That's correct — move to Task 2. - [ ] **Step 4: Commit scaffold** ```bash git add ingestion/ git commit -m "chore: scaffold ingestion Go module" ``` --- ## Task 2: ingestion search package **Files:** - Create: `ingestion/internal/search/search.go` - Create: `ingestion/internal/search/search_test.go` - [ ] **Step 1: Write the failing test** ```go // ingestion/internal/search/search_test.go package search_test import ( "os" "path/filepath" "testing" "github.com/mathiasbq/hyperguild/ingestion/internal/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSearch_ReturnsMatchingPages(t *testing.T) { dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755)) // Write a concept page mentioning "retry" require.NoError(t, os.WriteFile( filepath.Join(dir, "wiki", "concepts", "retry-logic.md"), []byte("---\ntitle: Retry Logic\ndomain: software\n---\n\nRetry logic handles transient failures by re-attempting operations.\n"), 0o644, )) // Write an unrelated page require.NoError(t, os.WriteFile( filepath.Join(dir, "wiki", "concepts", "database.md"), []byte("---\ntitle: Database\ndomain: software\n---\n\nA database stores structured data.\n"), 0o644, )) results, err := search.Query(dir, "retry transient", 5) require.NoError(t, err) require.Len(t, results, 1) assert.Equal(t, "wiki/concepts/retry-logic.md", results[0].Path) assert.Equal(t, "Retry Logic", results[0].Title) assert.Greater(t, results[0].Score, 0) assert.Contains(t, results[0].Excerpt, "Retry") } func TestSearch_RespectsLimit(t *testing.T) { dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755)) for i := 0; i < 5; i++ { require.NoError(t, os.WriteFile( filepath.Join(dir, "wiki", "concepts", fmt.Sprintf("page-%d.md", i)), []byte(fmt.Sprintf("---\ntitle: Page %d\n---\n\nThis page mentions retry.\n", i)), 0o644, )) } results, err := search.Query(dir, "retry", 3) require.NoError(t, err) assert.LessOrEqual(t, len(results), 3) } ``` Add `"fmt"` import. Run: ```bash cd ingestion && go test ./internal/search/... 2>&1 ``` Expected: FAIL — `search` package does not exist. - [ ] **Step 2: Implement search.go** ```go // ingestion/internal/search/search.go package search import ( "bufio" "os" "path/filepath" "sort" "strings" ) // Result is a single search hit from the brain wiki. type Result struct { Path string `json:"path"` Title string `json:"title"` Excerpt string `json:"excerpt"` Score int `json:"score"` } // Query searches all .md files under brainDir/wiki/ for pages containing // any of the whitespace-separated terms in query. Returns up to limit results // sorted by score descending. func Query(brainDir, query string, limit int) ([]Result, error) { if limit <= 0 { limit = 5 } terms := strings.Fields(strings.ToLower(query)) if len(terms) == 0 { return nil, nil } var results []Result err := filepath.WalkDir(filepath.Join(brainDir, "wiki"), func(path string, d os.DirEntry, err error) error { if err != nil || d.IsDir() || !strings.HasSuffix(path, ".md") { return err } content, err := os.ReadFile(path) if err != nil { return nil // skip unreadable files } lower := strings.ToLower(string(content)) score := 0 for _, term := range terms { score += strings.Count(lower, term) } if score == 0 { return nil } rel, _ := filepath.Rel(brainDir, path) rel = filepath.ToSlash(rel) results = append(results, Result{ Path: rel, Title: extractTitle(string(content), d.Name()), Excerpt: excerpt(string(content), 300), Score: score, }) return nil }) if err != nil { return nil, err } sort.Slice(results, func(i, j int) bool { return results[i].Score > results[j].Score }) if len(results) > limit { results = results[:limit] } return results, nil } func extractTitle(content, filename string) string { scanner := bufio.NewScanner(strings.NewReader(content)) inFrontmatter := false for scanner.Scan() { line := scanner.Text() if strings.TrimSpace(line) == "---" { if !inFrontmatter { inFrontmatter = true continue } break } if inFrontmatter { key, val, ok := strings.Cut(line, ":") if ok && strings.TrimSpace(key) == "title" { return strings.Trim(strings.TrimSpace(val), `"'`) } } } return strings.TrimSuffix(filename, ".md") } func excerpt(content string, maxLen int) string { // Skip frontmatter, return first maxLen chars of body. parts := strings.SplitN(content, "---", 3) body := content if len(parts) == 3 { body = strings.TrimSpace(parts[2]) } if len(body) > maxLen { return body[:maxLen] + "…" } return body } ``` - [ ] **Step 3: Run tests — expect PASS** ```bash cd ingestion && go test ./internal/search/... -v 2>&1 ``` Expected: PASS (2 tests). - [ ] **Step 4: Commit** ```bash git add ingestion/internal/search/ git commit -m "feat(ingestion): add full-text wiki search package" ``` --- ## Task 3: ingestion API handler **Files:** - Create: `ingestion/internal/api/handler.go` - Create: `ingestion/internal/api/handler_test.go` - [ ] **Step 1: Write the failing tests** ```go // ingestion/internal/api/handler_test.go package api_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "github.com/mathiasbq/hyperguild/ingestion/internal/api" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "log/slog" ) func setup(t *testing.T) (string, *api.Handler) { t.Helper() dir := t.TempDir() require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755)) require.NoError(t, os.MkdirAll(filepath.Join(dir, "raw"), 0o755)) require.NoError(t, os.WriteFile( filepath.Join(dir, "wiki", "concepts", "tdd.md"), []byte("---\ntitle: TDD\ndomain: software\n---\n\nTest-driven development is a discipline.\n"), 0o644, )) logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) return dir, api.NewHandler(dir, logger) } func TestQuery_ReturnsResults(t *testing.T) { _, h := setup(t) body, _ := json.Marshal(map[string]any{"query": "test driven", "limit": 5}) req := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Query(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var resp map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) results := resp["results"].([]any) assert.NotEmpty(t, results) } func TestWrite_CreatesRawFile(t *testing.T) { dir, h := setup(t) body, _ := json.Marshal(map[string]any{ "content": "# Test note\n\nSome content.", "filename": "test-note.md", }) req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Write(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var resp map[string]string require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp)) assert.NotEmpty(t, resp["path"]) written := filepath.Join(dir, "raw", "test-note.md") content, err := os.ReadFile(written) require.NoError(t, err) assert.Contains(t, string(content), "Some content.") } func TestWrite_GeneratesFilenameIfAbsent(t *testing.T) { dir, h := setup(t) body, _ := json.Marshal(map[string]any{"content": "auto name"}) req := httptest.NewRequest(http.MethodPost, "/write", bytes.NewReader(body)) rec := httptest.NewRecorder() h.Write(rec, req) assert.Equal(t, http.StatusOK, rec.Code) entries, _ := os.ReadDir(filepath.Join(dir, "raw")) assert.Len(t, entries, 1) assert.True(t, strings.HasSuffix(entries[0].Name(), ".md")) } ``` Run: ```bash cd ingestion && go test ./internal/api/... 2>&1 ``` Expected: FAIL — package `api` does not exist. - [ ] **Step 2: Implement handler.go** ```go // ingestion/internal/api/handler.go package api import ( "encoding/json" "fmt" "log/slog" "net/http" "os" "path/filepath" "time" "github.com/mathiasbq/hyperguild/ingestion/internal/search" ) // Handler serves the ingestion HTTP API. type Handler struct { brainDir string logger *slog.Logger } // NewHandler constructs a Handler. brainDir is the absolute path to brain/. func NewHandler(brainDir string, logger *slog.Logger) *Handler { return &Handler{brainDir: brainDir, logger: logger} } type queryRequest struct { Query string `json:"query"` Domain string `json:"domain,omitempty"` Limit int `json:"limit,omitempty"` } type writeRequest struct { Content string `json:"content"` Filename string `json:"filename,omitempty"` } // Query handles POST /query — full-text search across the brain wiki. func (h *Handler) Query(w http.ResponseWriter, r *http.Request) { var req queryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if req.Limit == 0 { req.Limit = 5 } results, err := search.Query(h.brainDir, req.Query, req.Limit) if err != nil { h.logger.Error("query failed", "err", err) http.Error(w, "search error", http.StatusInternalServerError) return } writeJSON(w, map[string]any{"results": results}) } // Write handles POST /write — write raw content to brain/raw/. func (h *Handler) Write(w http.ResponseWriter, r *http.Request) { var req writeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } if req.Content == "" { http.Error(w, "content is required", http.StatusBadRequest) return } filename := req.Filename if filename == "" { filename = fmt.Sprintf("%s-auto.md", time.Now().UTC().Format("2006-01-02-150405")) } rawDir := filepath.Join(h.brainDir, "raw") if err := os.MkdirAll(rawDir, 0o755); err != nil { http.Error(w, "failed to create raw dir", http.StatusInternalServerError) return } dest := filepath.Join(rawDir, filepath.Base(filename)) if err := os.WriteFile(dest, []byte(req.Content), 0o644); err != nil { h.logger.Error("write failed", "err", err) http.Error(w, "write error", http.StatusInternalServerError) return } rel, _ := filepath.Rel(h.brainDir, dest) writeJSON(w, map[string]string{"path": filepath.ToSlash(rel)}) } func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } ``` - [ ] **Step 3: Run tests — expect PASS** ```bash cd ingestion && go test ./internal/api/... -v 2>&1 ``` Expected: PASS (3 tests). - [ ] **Step 4: Verify full build** ```bash cd ingestion && go build ./... 2>&1 ``` Expected: clean. - [ ] **Step 5: Commit** ```bash git add ingestion/internal/ git commit -m "feat(ingestion): add query and write HTTP handlers" ``` --- ## Task 4: internal/tier — tier detection **Files:** - Create: `internal/tier/tier.go` - Create: `internal/tier/tier_test.go` - [ ] **Step 1: Write the failing tests** ```go // internal/tier/tier_test.go package tier_test import ( "context" "net/http" "net/http/httptest" "testing" "github.com/mathiasbq/supervisor/internal/tier" "github.com/stretchr/testify/assert" ) func TestDetect_Tier1_WhenBothReachable(t *testing.T) { anthropic := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer anthropic.Close() litellm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer litellm.Close() info := tier.Detect(context.Background(), anthropic.URL, litellm.URL) assert.Equal(t, tier.Full, info.Tier) assert.Equal(t, "full-online", info.Label) assert.True(t, info.ManagedAgents) } func TestDetect_Tier2_WhenOnlyLiteLLMReachable(t *testing.T) { litellm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) defer litellm.Close() info := tier.Detect(context.Background(), "http://127.0.0.1:1", litellm.URL) assert.Equal(t, tier.LANOnly, info.Tier) assert.Equal(t, "lan-only", info.Label) assert.False(t, info.ManagedAgents) } func TestDetect_Tier3_WhenNeitherReachable(t *testing.T) { info := tier.Detect(context.Background(), "http://127.0.0.1:1", "http://127.0.0.1:2") assert.Equal(t, tier.Airplane, info.Tier) assert.Equal(t, "airplane", info.Label) assert.False(t, info.ManagedAgents) } ``` Run: ```bash go test ./internal/tier/... 2>&1 ``` Expected: FAIL — package does not exist. - [ ] **Step 2: Implement tier.go** ```go // internal/tier/tier.go package tier import ( "context" "net/http" "time" ) // Tier represents the current operating capability level. type Tier int const ( Full Tier = 1 // internet + Anthropic API reachable LANOnly Tier = 2 // LiteLLM on LAN reachable, no internet Airplane Tier = 3 // no network ) // Info describes the current operating tier. type Info struct { Tier Tier `json:"tier"` Label string `json:"label"` AvailableModels []string `json:"available_models"` ManagedAgents bool `json:"managed_agents"` } // Detect probes the Anthropic endpoint and LiteLLM and returns the current tier. // probeTimeout is 2 seconds per probe. func Detect(ctx context.Context, anthropicProbe, liteLLMBaseURL string) Info { client := &http.Client{Timeout: 2 * time.Second} if probe(ctx, client, anthropicProbe) { return Info{ Tier: Full, Label: "full-online", ManagedAgents: true, } } if probe(ctx, client, liteLLMBaseURL) { return Info{ Tier: LANOnly, Label: "lan-only", ManagedAgents: false, } } return Info{ Tier: Airplane, Label: "airplane", ManagedAgents: false, } } func probe(ctx context.Context, client *http.Client, url string) bool { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return false } resp, err := client.Do(req) if err != nil { return false } resp.Body.Close() return true } ``` - [ ] **Step 3: Run tests — expect PASS** ```bash go test ./internal/tier/... -v 2>&1 ``` Expected: PASS (3 tests). Note: Tier2/Tier3 tests will take ~4s due to connection timeouts on port 1/2. - [ ] **Step 4: Commit** ```bash git add internal/tier/ git commit -m "feat: add tier detection package" ``` --- ## Task 5: internal/session — session log **Files:** - Create: `internal/session/session.go` - Create: `internal/session/session_test.go` - [ ] **Step 1: Write the failing tests** ```go // internal/session/session_test.go package session_test import ( "encoding/json" "os" "path/filepath" "testing" "time" "github.com/mathiasbq/supervisor/internal/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAppend_WritesJSONLEntry(t *testing.T) { dir := t.TempDir() entry := session.Entry{ SessionID: "test-session-1", Timestamp: time.Now().UTC(), Skill: "tdd_green", Phase: "green", ProjectRoot: "/tmp/myproject", FinalStatus: "pass", ModelUsed: "ollama/qwen3", DurationMs: 5000, } require.NoError(t, session.Append(dir, "test-session-1", entry)) path := filepath.Join(dir, "test-session-1.jsonl") data, err := os.ReadFile(path) require.NoError(t, err) var got session.Entry require.NoError(t, json.Unmarshal(data, &got)) assert.Equal(t, "test-session-1", got.SessionID) assert.Equal(t, "tdd_green", got.Skill) assert.Equal(t, "pass", got.FinalStatus) } func TestAppend_AppendsMultipleEntries(t *testing.T) { dir := t.TempDir() for i := 0; i < 3; i++ { require.NoError(t, session.Append(dir, "s1", session.Entry{ SessionID: "s1", Timestamp: time.Now().UTC(), Skill: "tdd_red", FinalStatus: "pass", })) } entries, err := session.Read(dir, "s1") require.NoError(t, err) assert.Len(t, entries, 3) } func TestRead_EmptyWhenNoFile(t *testing.T) { dir := t.TempDir() entries, err := session.Read(dir, "missing") require.NoError(t, err) assert.Empty(t, entries) } ``` Run: ```bash go test ./internal/session/... 2>&1 ``` Expected: FAIL. - [ ] **Step 2: Implement session.go** ```go // internal/session/session.go package session import ( "bufio" "encoding/json" "fmt" "os" "path/filepath" "time" ) // Entry is one skill invocation record, appended to the session JSONL log. type Entry struct { SessionID string `json:"session_id"` Timestamp time.Time `json:"timestamp"` Skill string `json:"skill"` Phase string `json:"phase,omitempty"` ProjectRoot string `json:"project_root,omitempty"` Input json.RawMessage `json:"input,omitempty"` Attempts []Attempt `json:"attempts,omitempty"` FinalStatus string `json:"final_status"` FilePath string `json:"file_path,omitempty"` ModelUsed string `json:"model_used,omitempty"` DurationMs int64 `json:"duration_ms,omitempty"` } // Attempt represents one subprocess invocation within a skill call. type Attempt struct { Attempt int `json:"attempt"` Model string `json:"model"` OutputSummary string `json:"output_summary,omitempty"` RunnerOutput string `json:"runner_output,omitempty"` Verified bool `json:"verified"` } // Append writes entry as a single JSON line to sessionsDir/{sessionID}.jsonl. func Append(sessionsDir, sessionID string, entry Entry) error { if err := os.MkdirAll(sessionsDir, 0o755); err != nil { return fmt.Errorf("create sessions dir: %w", err) } path := filepath.Join(sessionsDir, sessionID+".jsonl") f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("open session log: %w", err) } defer f.Close() line, err := json.Marshal(entry) if err != nil { return fmt.Errorf("marshal entry: %w", err) } _, err = fmt.Fprintf(f, "%s\n", line) return err } // Read returns all entries for sessionID. Returns empty slice if no log exists. func Read(sessionsDir, sessionID string) ([]Entry, error) { path := filepath.Join(sessionsDir, sessionID+".jsonl") f, err := os.Open(path) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, fmt.Errorf("open session log: %w", err) } defer f.Close() var entries []Entry scanner := bufio.NewScanner(f) for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 { continue } var e Entry if err := json.Unmarshal(line, &e); err != nil { return nil, fmt.Errorf("parse entry: %w", err) } entries = append(entries, e) } return entries, scanner.Err() } ``` - [ ] **Step 3: Run tests — expect PASS** ```bash go test ./internal/session/... -v 2>&1 ``` Expected: PASS (3 tests). - [ ] **Step 4: Commit** ```bash git add internal/session/ git commit -m "feat: add session log package (append/read JSONL)" ``` --- ## Task 6: brain skill (brain_query, brain_write MCP tools) **Files:** - Create: `internal/skills/brain/skill.go` - Create: `internal/skills/brain/handlers.go` - Create: `internal/skills/brain/handlers_test.go` - [ ] **Step 1: Write the failing tests** ```go // internal/skills/brain/handlers_test.go package brain_test import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/mathiasbq/supervisor/internal/skills/brain" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHandle_BrainQuery_CallsIngestServer(t *testing.T) { called := false srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/query", r.URL.Path) called = true json.NewEncoder(w).Encode(map[string]any{ "results": []map[string]any{ {"path": "wiki/concepts/tdd.md", "title": "TDD", "excerpt": "Test-driven development.", "score": 3}, }, }) })) defer srv.Close() s := brain.New(brain.Config{IngestBaseURL: srv.URL}) args, _ := json.Marshal(map[string]string{"query": "test driven development"}) out, err := s.Handle(context.Background(), "brain_query", args) require.NoError(t, err) assert.True(t, called) var result map[string]any require.NoError(t, json.Unmarshal(out, &result)) results := result["results"].([]any) assert.Len(t, results, 1) } func TestHandle_BrainWrite_CallsIngestServer(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/write", r.URL.Path) json.NewEncoder(w).Encode(map[string]string{"path": "raw/test.md"}) })) defer srv.Close() s := brain.New(brain.Config{IngestBaseURL: srv.URL}) args, _ := json.Marshal(map[string]string{"content": "# Test\n\nSome learning.", "type": "concept"}) out, err := s.Handle(context.Background(), "brain_write", args) require.NoError(t, err) var result map[string]string require.NoError(t, json.Unmarshal(out, &result)) assert.Equal(t, "raw/test.md", result["path"]) } func TestHandle_UnknownTool_ReturnsError(t *testing.T) { s := brain.New(brain.Config{IngestBaseURL: "http://localhost:3300"}) _, err := s.Handle(context.Background(), "brain_unknown", nil) assert.Error(t, err) } ``` Run: ```bash go test ./internal/skills/brain/... 2>&1 ``` Expected: FAIL. - [ ] **Step 2: Implement skill.go** ```go // internal/skills/brain/skill.go package brain import ( "context" "encoding/json" "github.com/mathiasbq/supervisor/internal/registry" ) // Config holds brain skill configuration. type Config struct { IngestBaseURL string // base URL of the ingestion HTTP server } // Skill implements registry.Skill for brain_query and brain_write. type Skill struct { cfg Config } func New(cfg Config) *Skill { return &Skill{cfg: cfg} } func (s *Skill) Name() string { return "brain" } func (s *Skill) Tools() []registry.ToolDef { schema := func(required []string, props map[string]any) json.RawMessage { b, _ := json.Marshal(map[string]any{"type": "object", "required": required, "properties": props}) return b } str := map[string]any{"type": "string"} num := map[string]any{"type": "integer"} return []registry.ToolDef{ { Name: "brain_query", Description: "Search the hyperguild brain wiki for relevant knowledge. Call this before starting any significant task.", InputSchema: schema([]string{"query"}, map[string]any{ "query": str, "domain": str, "limit": num, }), }, { Name: "brain_write", Description: "Write a raw knowledge note to the brain for later ingestion into the wiki.", InputSchema: schema([]string{"content"}, map[string]any{ "content": str, "type": str, "domain": str, "filename": str, }), }, } } ``` - [ ] **Step 3: Implement handlers.go** ```go // internal/skills/brain/handlers.go package brain import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" ) func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { switch tool { case "brain_query": return s.query(ctx, args) case "brain_write": return s.write(ctx, args) default: return nil, fmt.Errorf("unknown brain tool: %s", tool) } } type queryArgs struct { Query string `json:"query"` Domain string `json:"domain,omitempty"` Limit int `json:"limit,omitempty"` } func (s *Skill) query(ctx context.Context, args json.RawMessage) (json.RawMessage, error) { var a queryArgs if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } if a.Query == "" { return nil, fmt.Errorf("query is required") } if a.Limit == 0 { a.Limit = 5 } return s.post(ctx, "/query", a) } type writeArgs struct { Content string `json:"content"` Type string `json:"type,omitempty"` Domain string `json:"domain,omitempty"` Filename string `json:"filename,omitempty"` } func (s *Skill) write(ctx context.Context, args json.RawMessage) (json.RawMessage, error) { var a writeArgs if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } if a.Content == "" { return nil, fmt.Errorf("content is required") } return s.post(ctx, "/write", map[string]string{ "content": a.Content, "filename": a.Filename, }) } func (s *Skill) post(ctx context.Context, path string, body any) (json.RawMessage, error) { b, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.IngestBaseURL+path, bytes.NewReader(b)) if err != nil { return nil, fmt.Errorf("build request: %w", err) } req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("call ingestion server: %w", err) } defer resp.Body.Close() out, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("ingestion server returned %d: %s", resp.StatusCode, out) } return json.RawMessage(out), nil } ``` - [ ] **Step 4: Run tests — expect PASS** ```bash go test ./internal/skills/brain/... -v 2>&1 ``` Expected: PASS (3 tests). - [ ] **Step 5: Commit** ```bash git add internal/skills/brain/ git commit -m "feat: add brain_query and brain_write MCP tools" ``` --- ## Task 7: org skill (tier tool) + sessionlog skill (session_log tool) **Files:** - Create: `internal/skills/org/skill.go` - Create: `internal/skills/org/handlers.go` - Create: `internal/skills/org/handlers_test.go` - Create: `internal/skills/sessionlog/skill.go` - Create: `internal/skills/sessionlog/handlers.go` - Create: `internal/skills/sessionlog/handlers_test.go` - [ ] **Step 1: Write failing tests for org skill** ```go // internal/skills/org/handlers_test.go package org_test import ( "context" "encoding/json" "testing" "github.com/mathiasbq/supervisor/internal/skills/org" "github.com/mathiasbq/supervisor/internal/tier" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHandle_Tier_ReturnsTierInfo(t *testing.T) { s := org.New(org.Config{ TierFn: func(ctx context.Context) tier.Info { return tier.Info{Tier: tier.LANOnly, Label: "lan-only", ManagedAgents: false} }, }) out, err := s.Handle(context.Background(), "tier", nil) require.NoError(t, err) var info tier.Info require.NoError(t, json.Unmarshal(out, &info)) assert.Equal(t, tier.LANOnly, info.Tier) assert.Equal(t, "lan-only", info.Label) assert.False(t, info.ManagedAgents) } ``` Run: ```bash go test ./internal/skills/org/... 2>&1 ``` Expected: FAIL. - [ ] **Step 2: Implement org skill** ```go // internal/skills/org/skill.go package org import ( "context" "encoding/json" "github.com/mathiasbq/supervisor/internal/registry" "github.com/mathiasbq/supervisor/internal/tier" ) // TierFn is a function that returns the current tier. Injected for testability. type TierFn func(ctx context.Context) tier.Info // Config holds org skill configuration. type Config struct { TierFn TierFn } // Skill implements registry.Skill for the tier tool. type Skill struct { cfg Config } func New(cfg Config) *Skill { return &Skill{cfg: cfg} } func (s *Skill) Name() string { return "org" } func (s *Skill) Tools() []registry.ToolDef { return []registry.ToolDef{ { Name: "tier", Description: "Returns the current operating tier: 1=full-online (Claude+Ollama+Managed Agents), 2=lan-only (Ollama only), 3=airplane (minimal). Call at session start to know which models and capabilities are available.", InputSchema: json.RawMessage(`{"type":"object","properties":{}}`), }, } } ``` ```go // internal/skills/org/handlers.go package org import ( "context" "encoding/json" "fmt" ) func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { if tool != "tier" { return nil, fmt.Errorf("unknown org tool: %s", tool) } info := s.cfg.TierFn(ctx) b, err := json.Marshal(info) if err != nil { return nil, fmt.Errorf("marshal tier info: %w", err) } return b, nil } ``` - [ ] **Step 3: Write failing tests for sessionlog skill** ```go // internal/skills/sessionlog/handlers_test.go package sessionlog_test import ( "context" "encoding/json" "os" "path/filepath" "testing" "github.com/mathiasbq/supervisor/internal/skills/sessionlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHandle_SessionLog_AppendsEntry(t *testing.T) { dir := t.TempDir() s := sessionlog.New(sessionlog.Config{SessionsDir: dir}) args, _ := json.Marshal(map[string]any{ "session_id": "sess-abc", "skill": "tdd_green", "final_status": "pass", "model_used": "ollama/qwen3", "duration_ms": 3000, }) out, err := s.Handle(context.Background(), "session_log", args) require.NoError(t, err) var result map[string]string require.NoError(t, json.Unmarshal(out, &result)) assert.Equal(t, "ok", result["status"]) // Verify file written data, err := os.ReadFile(filepath.Join(dir, "sess-abc.jsonl")) require.NoError(t, err) assert.Contains(t, string(data), "tdd_green") } func TestHandle_SessionLog_RequiresSessionID(t *testing.T) { s := sessionlog.New(sessionlog.Config{SessionsDir: t.TempDir()}) args, _ := json.Marshal(map[string]any{"skill": "tdd_red"}) _, err := s.Handle(context.Background(), "session_log", args) assert.Error(t, err) } ``` Run: ```bash go test ./internal/skills/sessionlog/... 2>&1 ``` Expected: FAIL. - [ ] **Step 4: Implement sessionlog skill** ```go // internal/skills/sessionlog/skill.go package sessionlog import ( "context" "encoding/json" "github.com/mathiasbq/supervisor/internal/registry" ) // Config holds sessionlog skill configuration. type Config struct { SessionsDir string // path to brain/sessions/ } // Skill implements registry.Skill for the session_log tool. type Skill struct { cfg Config } func New(cfg Config) *Skill { return &Skill{cfg: cfg} } func (s *Skill) Name() string { return "sessionlog" } func (s *Skill) Tools() []registry.ToolDef { return []registry.ToolDef{ { Name: "session_log", Description: "Append a structured entry to the current session log. Call after each skill invocation completes to record what happened for retrospective and training data extraction.", InputSchema: json.RawMessage(`{ "type": "object", "required": ["session_id"], "properties": { "session_id": {"type": "string"}, "skill": {"type": "string"}, "phase": {"type": "string"}, "project_root": {"type": "string"}, "final_status": {"type": "string"}, "file_path": {"type": "string"}, "model_used": {"type": "string"}, "duration_ms": {"type": "integer"}, "message": {"type": "string"} } }`), }, } } ``` ```go // internal/skills/sessionlog/handlers.go package sessionlog import ( "context" "encoding/json" "fmt" "time" "github.com/mathiasbq/supervisor/internal/session" ) type logArgs struct { SessionID string `json:"session_id"` Skill string `json:"skill"` Phase string `json:"phase,omitempty"` ProjectRoot string `json:"project_root,omitempty"` FinalStatus string `json:"final_status,omitempty"` FilePath string `json:"file_path,omitempty"` ModelUsed string `json:"model_used,omitempty"` DurationMs int64 `json:"duration_ms,omitempty"` Message string `json:"message,omitempty"` } func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { if tool != "session_log" { return nil, fmt.Errorf("unknown sessionlog tool: %s", tool) } var a logArgs if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } if a.SessionID == "" { return nil, fmt.Errorf("session_id is required") } entry := session.Entry{ SessionID: a.SessionID, Timestamp: time.Now().UTC(), Skill: a.Skill, Phase: a.Phase, ProjectRoot: a.ProjectRoot, FinalStatus: a.FinalStatus, FilePath: a.FilePath, ModelUsed: a.ModelUsed, DurationMs: a.DurationMs, } if err := session.Append(s.cfg.SessionsDir, a.SessionID, entry); err != nil { return nil, fmt.Errorf("append session log: %w", err) } b, _ := json.Marshal(map[string]string{"status": "ok", "session_id": a.SessionID}) return b, nil } ``` - [ ] **Step 5: Run all new tests** ```bash go test ./internal/skills/org/... ./internal/skills/sessionlog/... -v 2>&1 ``` Expected: PASS (all tests). - [ ] **Step 6: Commit** ```bash git add internal/skills/org/ internal/skills/sessionlog/ git commit -m "feat: add tier and session_log MCP tools" ``` --- ## Task 8: retrospective skill **Files:** - Create: `internal/skills/retrospective/skill.go` - Create: `internal/skills/retrospective/handlers.go` - Create: `internal/skills/retrospective/handlers_test.go` - [ ] **Step 1: Write the failing tests** ```go // internal/skills/retrospective/handlers_test.go package retrospective_test import ( "context" "encoding/json" "testing" iexec "github.com/mathiasbq/supervisor/internal/exec" "github.com/mathiasbq/supervisor/internal/skills/retrospective" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHandle_Retrospective_RequiresSessionID(t *testing.T) { s := retrospective.New(retrospective.Config{}) _, err := s.Handle(context.Background(), "retrospective", json.RawMessage(`{}`)) assert.Error(t, err) assert.Contains(t, err.Error(), "session_id") } func TestHandle_Retrospective_BuildsPromptWithSessionLog(t *testing.T) { var capturedReq iexec.Request s := retrospective.New(retrospective.Config{ SkillPrompt: "retrospective discipline", DefaultModel: "ollama/test", SessionsDir: "testdata", ExecutorFn: func(_ context.Context, req iexec.Request) (iexec.Result, error) { capturedReq = req return iexec.Result{ Status: "pass", Phase: "retrospective", Skill: "retrospective", Verified: true, Message: "wrote 2 entries to brain", }, nil }, }) args, _ := json.Marshal(map[string]string{"session_id": "empty-session"}) out, err := s.Handle(context.Background(), "retrospective", args) require.NoError(t, err) var result iexec.Result require.NoError(t, json.Unmarshal(out, &result)) assert.Equal(t, "pass", result.Status) assert.Contains(t, capturedReq.SkillPrompt, "retrospective discipline") assert.Contains(t, capturedReq.TaskPrompt, "empty-session") } ``` Run: ```bash go test ./internal/skills/retrospective/... 2>&1 ``` Expected: FAIL. - [ ] **Step 2: Implement skill.go** ```go // internal/skills/retrospective/skill.go package retrospective import ( "context" "encoding/json" iexec "github.com/mathiasbq/supervisor/internal/exec" "github.com/mathiasbq/supervisor/internal/registry" ) // ExecutorFn allows injecting a test double. type ExecutorFn func(ctx context.Context, req iexec.Request) (iexec.Result, error) // Config holds retrospective skill configuration. type Config struct { SkillPrompt string DefaultModel string SessionsDir string // path to brain/sessions/ ExecutorFn ExecutorFn } // Skill implements registry.Skill for the retrospective tool. type Skill struct { cfg Config } func New(cfg Config) *Skill { return &Skill{cfg: cfg} } func (s *Skill) Name() string { return "retrospective" } func (s *Skill) Tools() []registry.ToolDef { return []registry.ToolDef{ { Name: "retrospective", Description: "Run a retrospective on a completed session. Reads the session log, identifies novel learnings, and writes structured entries to the brain for ingestion. Call at the end of each coding session.", InputSchema: json.RawMessage(`{ "type": "object", "required": ["session_id"], "properties": { "session_id": {"type": "string"}, "model": {"type": "string"} } }`), }, } } ``` - [ ] **Step 3: Implement handlers.go** ```go // internal/skills/retrospective/handlers.go package retrospective import ( "context" "encoding/json" "fmt" iexec "github.com/mathiasbq/supervisor/internal/exec" "github.com/mathiasbq/supervisor/internal/session" ) type retroArgs struct { SessionID string `json:"session_id"` Model string `json:"model,omitempty"` } func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { if tool != "retrospective" { return nil, fmt.Errorf("unknown retrospective tool: %s", tool) } var a retroArgs if err := json.Unmarshal(args, &a); err != nil { return nil, fmt.Errorf("parse args: %w", err) } if a.SessionID == "" { return nil, fmt.Errorf("session_id is required") } model := a.Model if model == "" { model = s.cfg.DefaultModel } // Read session log entries. entries, err := session.Read(s.cfg.SessionsDir, a.SessionID) if err != nil { return nil, fmt.Errorf("read session log: %w", err) } logJSON, _ := json.MarshalIndent(entries, "", " ") taskPrompt := fmt.Sprintf( "SESSION_ID: %s\n\nSESSION_LOG:\n%s\n\nReview this session log. Identify what is novel or worth preserving as organizational knowledge. Write structured entries to brain/raw/ via brain_write. Return JSON result when done.", a.SessionID, string(logJSON), ) if s.cfg.ExecutorFn == nil { return nil, fmt.Errorf("no executor configured") } result, err := s.cfg.ExecutorFn(ctx, iexec.Request{ SkillPrompt: s.cfg.SkillPrompt, TaskPrompt: taskPrompt, Model: model, Tools: "Bash,Read,Write", }) if err != nil { return nil, fmt.Errorf("retrospective worker: %w", err) } b, err := json.Marshal(result) if err != nil { return nil, fmt.Errorf("marshal result: %w", err) } return b, nil } ``` - [ ] **Step 4: Run tests — expect PASS** ```bash go test ./internal/skills/retrospective/... -v 2>&1 ``` Expected: PASS (2 tests). - [ ] **Step 5: Commit** ```bash git add internal/skills/retrospective/ git commit -m "feat: add retrospective MCP tool" ``` --- ## Task 9: config files and brain directory structure **Files:** - Create: `config/supervisor/protocols.md` - Create: `config/supervisor/retrospective.md` - Create: `brain/` directory structure (gitkeep files) - [ ] **Step 1: Write protocols.md** ```markdown # The Hyperguild Way These protocols are injected into every worker invocation. They define how you behave as a member of the hyperguild. ## Output contract Every response is raw JSON matching the response schema. No preamble, no prose, no markdown. Malformed output is treated as a failed invocation. ## Quality gate `verified: true` only when a subprocess exit code confirms the outcome. Never self-assess. "I think the tests pass" is not verified. ## Escalation If stuck after 3 attempts, return `status: error` with a clear `message` explaining why. Do not retry silently. Do not fabricate a passing result. ## Working offline If brain context is absent from your prompt, proceed using your discipline file only. Note the gap in your `message` field: "no brain context available". ## Handoff format Structure your output so the next worker in a chain can consume it without transformation. Use the standard result schema. Do not add extra fields. ## Session logging The Go skill handler records your invocation in the session log automatically. You do not need to do this yourself. ``` - [ ] **Step 2: Write retrospective.md** ```markdown # Retrospective Worker Discipline You are the retrospective worker. Your job is to review a completed coding session and identify knowledge worth preserving in the hyperguild brain. ## What you receive - A session log in JSON format listing every skill invocation: what was attempted, what failed, what passed, how long it took. ## What you produce For each significant learning, call brain_write with a structured markdown note. Then return a JSON result summarising what you wrote. ## What is worth preserving - Patterns that worked and should be repeated - Failures that revealed something non-obvious about the codebase or the discipline - Decisions made during the session (architectural, structural, tooling) - Anything that contradicts or extends what the brain already knows ## What is NOT worth preserving - Routine TDD cycles with no surprises - Single-attempt passes with no interesting context - Mechanical operations (file moves, renames, formatting) ## Output format Return JSON matching the standard result schema: ```json { "status": "pass", "phase": "retrospective", "skill": "retrospective", "verified": true, "message": "wrote N entries to brain/raw/" } ``` `verified` is true when you successfully called brain_write at least once and received a confirmation. If the session had nothing worth writing, return `verified: true` with `message: "no novel learnings in this session"`. ``` - [ ] **Step 3: Create brain directory structure** ```bash mkdir -p brain/wiki/concepts brain/wiki/entities brain/wiki/sources mkdir -p brain/raw brain/sessions mkdir -p brain/training-data/sft brain/training-data/dpo brain/training-data/rl touch brain/wiki/concepts/.gitkeep brain/wiki/entities/.gitkeep brain/wiki/sources/.gitkeep touch brain/raw/.gitkeep brain/sessions/.gitkeep touch brain/training-data/sft/.gitkeep brain/training-data/dpo/.gitkeep brain/training-data/rl/.gitkeep ``` - [ ] **Step 4: Add brain/ to .gitignore exceptions** Edit `.gitignore` — add after the existing Binaries section: ```gitignore # Brain content — keep wiki and structure, exclude session logs and training data brain/sessions/*.jsonl brain/training-data/**/*.jsonl ``` - [ ] **Step 5: Commit** ```bash git add config/supervisor/protocols.md config/supervisor/retrospective.md brain/ git commit -m "feat: add protocols.md, retrospective discipline, and brain directory structure" ``` --- ## Task 10: update config and wire new skills in main.go **Files:** - Modify: `internal/config/config.go` - Modify: `cmd/supervisor/main.go` - Modify: `config/models.yaml` - Modify: `.env.example` - [ ] **Step 1: Extend config.go** Add to the `Config` struct and `Load()` in `internal/config/config.go`: ```go // Add to Config struct: IngestBaseURL string // INGEST_BASE_URL, default http://localhost:3300 SessionsDir string // SUPERVISOR_SESSIONS_DIR, default ./brain/sessions BrainDir string // SUPERVISOR_BRAIN_DIR, default ./brain // Add to Load(): cfg.IngestBaseURL = envOr("INGEST_BASE_URL", "http://localhost:3300") cfg.SessionsDir = envOr("SUPERVISOR_SESSIONS_DIR", "./brain/sessions") cfg.BrainDir = envOr("SUPERVISOR_BRAIN_DIR", "./brain") ``` - [ ] **Step 2: Update config_test.go to cover new fields** Add to `TestLoad_Defaults` in `internal/config/config_test.go`: ```go assert.Equal(t, "http://localhost:3300", cfg.IngestBaseURL) assert.Equal(t, "./brain/sessions", cfg.SessionsDir) assert.Equal(t, "./brain", cfg.BrainDir) ``` Run: ```bash go test ./internal/config/... -v 2>&1 ``` Expected: PASS. - [ ] **Step 3: Add retrospective model to config/models.yaml** ```yaml default: ollama/qwen3-coder-30b-tuned skills: tdd: ollama/qwen3-coder-30b-tuned review: ollama/devstral-tuned debug: ollama/deepseek-r1-tuned retrospective: ollama/qwen3-coder-30b-tuned trainer: ollama/qwen3-coder-30b-tuned ``` - [ ] **Step 4: Rewrite cmd/supervisor/main.go** ```go // cmd/supervisor/main.go package main import ( "context" "log/slog" "net/http" "os" "github.com/mathiasbq/supervisor/internal/config" iexec "github.com/mathiasbq/supervisor/internal/exec" "github.com/mathiasbq/supervisor/internal/mcp" "github.com/mathiasbq/supervisor/internal/registry" "github.com/mathiasbq/supervisor/internal/skills/brain" "github.com/mathiasbq/supervisor/internal/skills/org" "github.com/mathiasbq/supervisor/internal/skills/retrospective" "github.com/mathiasbq/supervisor/internal/skills/sessionlog" "github.com/mathiasbq/supervisor/internal/skills/tdd" "github.com/mathiasbq/supervisor/internal/tier" ) func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) cfg, err := config.Load() if err != nil { logger.Error("load config", "err", err) os.Exit(1) } models, err := config.LoadModels(cfg.ModelsFile) if err != nil { logger.Error("load models", "err", err) os.Exit(1) } systemPrompt, err := os.ReadFile(cfg.ConfigDir + "/CLAUDE.md") if err != nil { logger.Error("read supervisor CLAUDE.md", "err", err) os.Exit(1) } tddPrompt, err := os.ReadFile(cfg.ConfigDir + "/tdd.md") if err != nil { logger.Error("read tdd.md", "err", err) os.Exit(1) } retroPrompt, err := os.ReadFile(cfg.ConfigDir + "/retrospective.md") if err != nil { logger.Error("read retrospective.md", "err", err) os.Exit(1) } executor := iexec.New(iexec.Config{ SystemPrompt: string(systemPrompt), LiteLLMBaseURL: cfg.LiteLLMBaseURL, LiteLLMAPIKey: cfg.LiteLLMAPIKey, }) tierFn := func(ctx context.Context) tier.Info { return tier.Detect(ctx, "https://api.anthropic.com", cfg.LiteLLMBaseURL) } reg := registry.New() reg.Register(tdd.New(tdd.Config{ SkillPrompt: string(tddPrompt), DefaultModel: models.Resolve("tdd", ""), ExecutorFn: executor.Run, })) reg.Register(brain.New(brain.Config{ IngestBaseURL: cfg.IngestBaseURL, })) reg.Register(org.New(org.Config{ TierFn: tierFn, })) reg.Register(sessionlog.New(sessionlog.Config{ SessionsDir: cfg.SessionsDir, })) reg.Register(retrospective.New(retrospective.Config{ SkillPrompt: string(retroPrompt), DefaultModel: models.Resolve("retrospective", ""), SessionsDir: cfg.SessionsDir, ExecutorFn: executor.Run, })) srv := mcp.NewServer(reg) mux := http.NewServeMux() mux.Handle("/mcp", srv) addr := ":" + cfg.Port logger.Info("supervisor starting", "addr", addr) if err := http.ListenAndServe(addr, mux); err != nil { logger.Error("server stopped", "err", err) os.Exit(1) } } ``` - [ ] **Step 5: Update .env.example** Add to `.env.example`: ```bash # Ingestion server INGEST_BASE_URL=http://localhost:3300 INGEST_PORT=3300 INGEST_BRAIN_DIR=./brain # Brain directories SUPERVISOR_SESSIONS_DIR=./brain/sessions SUPERVISOR_BRAIN_DIR=./brain ``` - [ ] **Step 6: Build to verify no compile errors** ```bash go build ./... 2>&1 ``` Expected: clean. - [ ] **Step 7: Run all tests** ```bash go test ./... 2>&1 ``` Expected: all PASS. - [ ] **Step 8: Commit** ```bash git add internal/config/ cmd/supervisor/main.go config/models.yaml .env.example git commit -m "feat: wire brain, org, sessionlog, retrospective skills into supervisor" ``` --- ## Task 11: Taskfile and MCP registration **Files:** - Modify: `Taskfile.yml` - Modify: `.context/mcp.json` - [ ] **Step 1: Add ingestion server tasks to Taskfile.yml** Add the following tasks to `Taskfile.yml`: ```yaml ingestion:build: desc: Build ingestion server binary cmds: - go build -o bin/ingestion-server ./cmd/server dir: ingestion ingestion:dev: desc: Run ingestion server in development mode env: INGEST_BRAIN_DIR: "{{.ROOT_DIR}}/brain" INGEST_PORT: "3300" cmds: - go run ./cmd/server dir: ingestion ingestion:test: desc: Run ingestion tests cmds: - go test ./... -v dir: ingestion dev:all: desc: Start both supervisor and ingestion server (requires two terminals) cmds: - echo "Start ingestion: task ingestion:dev" - echo "Start supervisor: task supervisor:dev" ``` - [ ] **Step 2: Update .context/mcp.json** Update `.context/mcp.json` to reflect the expanded tool set: ```json { "mcpServers": { "knowledge": { "url": "http://localhost:3100/mcp" }, "supervisor": { "url": "http://localhost:3200/mcp", "description": "Hyperguild SDO — skill workers (tdd, retrospective), brain tools (brain_query, brain_write), session logging, tier detection" } } } ``` - [ ] **Step 3: Commit** ```bash git add Taskfile.yml .context/mcp.json git commit -m "chore: add ingestion server tasks and update MCP registration" ``` --- ## Task 12: Integration smoke test Verify the full Phase 1 system works end-to-end. - [ ] **Step 1: Start the ingestion server** ```bash INGEST_BRAIN_DIR=./brain INGEST_PORT=3300 go run ./cmd/server & sleep 1 curl -s http://localhost:3300/query -d '{"query":"test"}' -H "Content-Type: application/json" | jq . ``` Expected: `{"results": []}` (brain is empty — that's correct). - [ ] **Step 2: Write a note to the brain** ```bash curl -s -X POST http://localhost:3300/write \ -H "Content-Type: application/json" \ -d '{"content": "# TDD Pattern\n\nAlways write the failing test first.", "filename": "tdd-pattern-test.md"}' | jq . ``` Expected: `{"path": "raw/tdd-pattern-test.md"}`. - [ ] **Step 3: Start the supervisor** ```bash SUPERVISOR_CONFIG_DIR=./config/supervisor \ SUPERVISOR_MODELS_FILE=./config/models.yaml \ SUPERVISOR_SESSIONS_DIR=./brain/sessions \ SUPERVISOR_BRAIN_DIR=./brain \ INGEST_BASE_URL=http://localhost:3300 \ go run ./cmd/supervisor/ & sleep 1 ``` - [ ] **Step 4: Verify tools/list includes all new tools** ```bash curl -s -X POST http://localhost:3200/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | jq '.result.tools[].name' ``` Expected output includes: ``` "tdd_red" "tdd_green" "tdd_refactor" "brain_query" "brain_write" "tier" "session_log" "retrospective" ``` - [ ] **Step 5: Call tier tool** ```bash curl -s -X POST http://localhost:3200/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"tier","arguments":{}}}' | jq . ``` Expected: valid tier response with `tier`, `label`, `managed_agents` fields. - [ ] **Step 6: Call brain_query** ```bash curl -s -X POST http://localhost:3200/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"brain_query","arguments":{"query":"TDD failing test"}}}' | jq . ``` Expected: results array containing the tdd-pattern-test.md note written in Step 2. - [ ] **Step 7: Call session_log** ```bash curl -s -X POST http://localhost:3200/mcp \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"session_log","arguments":{"session_id":"smoke-test","skill":"tdd_green","final_status":"pass","model_used":"test","duration_ms":1000}}}' | jq . ``` Expected: `{"status":"ok","session_id":"smoke-test"}`. Verify file: `cat brain/sessions/smoke-test.jsonl` — should contain one JSON line. - [ ] **Step 8: Stop servers and commit** ```bash pkill -f "go run ./cmd/server" 2>/dev/null pkill -f "go run ./cmd/supervisor" 2>/dev/null git add -A git commit -m "test: phase 1 integration smoke test passing" ``` --- ## Success Criteria - [ ] `go test ./...` passes in supervisor module - [ ] `go test ./...` passes in ingestion module - [ ] `tools/list` returns 8 tools: tdd_red, tdd_green, tdd_refactor, brain_query, brain_write, tier, session_log, retrospective - [ ] `brain_query` returns results after a note is written via `brain_write` - [ ] `session_log` appends JSONL entries to `brain/sessions/` - [ ] `tier` returns a valid JSON response with tier/label/managed_agents fields - [ ] `brain/training-data/` directory structure exists