brain_write with a custom filename omitted the .md extension, causing search to skip the file (search.go filters on HasSuffix .md). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
55 KiB
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 modulegithub.com/mathiasbq/hyperguild/ingestioningestion/cmd/server/main.go— HTTP server entry point (:3300)ingestion/internal/api/handler.go—/queryand/writehandlersingestion/internal/api/handler_test.goingestion/internal/search/search.go— full-text search across wiki filesingestion/internal/search/search_test.go
New — supervisor packages:
internal/tier/tier.go— tier detection by probing endpointsinternal/tier/tier_test.gointernal/session/session.go— append/read JSONL session logsinternal/session/session_test.gointernal/skills/brain/skill.go— brain_query + brain_write MCP toolsinternal/skills/brain/handlers.gointernal/skills/brain/handlers_test.gointernal/skills/org/skill.go— tier MCP toolinternal/skills/org/handlers.gointernal/skills/org/handlers_test.gointernal/skills/sessionlog/skill.go— session_log MCP toolinternal/skills/sessionlog/handlers.gointernal/skills/sessionlog/handlers_test.gointernal/skills/retrospective/skill.go— retrospective MCP toolinternal/skills/retrospective/handlers.gointernal/skills/retrospective/handlers_test.go
New — config files:
config/supervisor/protocols.mdconfig/supervisor/retrospective.mdbrain/wiki/concepts/.gitkeepbrain/wiki/entities/.gitkeepbrain/wiki/sources/.gitkeepbrain/raw/.gitkeepbrain/sessions/.gitkeepbrain/training-data/sft/.gitkeepbrain/training-data/dpo/.gitkeepbrain/training-data/rl/.gitkeep
Modified:
internal/skills/tdd/handlers.go— call session_log after each phaseinternal/config/config.go— add IngestBaseURL, SessionsDir, BrainDircmd/supervisor/main.go— wire new skillsconfig/models.yaml— add retrospective modelTaskfile.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:
cd ingestion && go mod tidy
- Step 2: Create the server entry point
// 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)
cd ingestion && go build ./... 2>&1
Expected: error about missing api package. That's correct — move to Task 2.
- Step 4: Commit scaffold
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
// 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:
cd ingestion && go test ./internal/search/... 2>&1
Expected: FAIL — search package does not exist.
- Step 2: Implement search.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
cd ingestion && go test ./internal/search/... -v 2>&1
Expected: PASS (2 tests).
- Step 4: Commit
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
// 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:
cd ingestion && go test ./internal/api/... 2>&1
Expected: FAIL — package api does not exist.
- Step 2: Implement handler.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
cd ingestion && go test ./internal/api/... -v 2>&1
Expected: PASS (3 tests).
- Step 4: Verify full build
cd ingestion && go build ./... 2>&1
Expected: clean.
- Step 5: Commit
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
// 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:
go test ./internal/tier/... 2>&1
Expected: FAIL — package does not exist.
- Step 2: Implement tier.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
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
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
// 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:
go test ./internal/session/... 2>&1
Expected: FAIL.
- Step 2: Implement session.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
go test ./internal/session/... -v 2>&1
Expected: PASS (3 tests).
- Step 4: Commit
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
// 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:
go test ./internal/skills/brain/... 2>&1
Expected: FAIL.
- Step 2: Implement skill.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
// 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
go test ./internal/skills/brain/... -v 2>&1
Expected: PASS (3 tests).
- Step 5: Commit
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
// 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:
go test ./internal/skills/org/... 2>&1
Expected: FAIL.
- Step 2: Implement org skill
// 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":{}}`),
},
}
}
// 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
// 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:
go test ./internal/skills/sessionlog/... 2>&1
Expected: FAIL.
- Step 4: Implement sessionlog skill
// 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"}
}
}`),
},
}
}
// 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
go test ./internal/skills/org/... ./internal/skills/sessionlog/... -v 2>&1
Expected: PASS (all tests).
- Step 6: Commit
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
// 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:
go test ./internal/skills/retrospective/... 2>&1
Expected: FAIL.
- Step 2: Implement skill.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
// 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
go test ./internal/skills/retrospective/... -v 2>&1
Expected: PASS (2 tests).
- Step 5: Commit
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
<!-- config/supervisor/protocols.md -->
# 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
<!-- config/supervisor/retrospective.md -->
# 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:
# Brain content — keep wiki and structure, exclude session logs and training data
brain/sessions/*.jsonl
brain/training-data/**/*.jsonl
- Step 5: Commit
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:
// 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:
assert.Equal(t, "http://localhost:3300", cfg.IngestBaseURL)
assert.Equal(t, "./brain/sessions", cfg.SessionsDir)
assert.Equal(t, "./brain", cfg.BrainDir)
Run:
go test ./internal/config/... -v 2>&1
Expected: PASS.
- Step 3: Add retrospective model to config/models.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
// 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:
# 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
go build ./... 2>&1
Expected: clean.
- Step 7: Run all tests
go test ./... 2>&1
Expected: all PASS.
- Step 8: Commit
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:
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:
{
"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
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
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
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
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
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
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
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
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
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 modulego test ./...passes in ingestion moduletools/listreturns 8 tools: tdd_red, tdd_green, tdd_refactor, brain_query, brain_write, tier, session_log, retrospectivebrain_queryreturns results after a note is written viabrain_writesession_logappends JSONL entries tobrain/sessions/tierreturns a valid JSON response with tier/label/managed_agents fieldsbrain/training-data/directory structure exists