6 Commits

Author SHA1 Message Date
Mathias Bergqvist
823de23213 feat(exec): log per-attempt chain verdicts for pass rate visibility
All checks were successful
cd / Build and deploy (push) Successful in 6s
CI / Lint / Test / Vet (push) Successful in 1m9s
CI / Mirror to GitHub (push) Successful in 4s
2026-04-22 15:40:15 +02:00
Mathias Bergqvist
78d3939caa feat(config): wire protocols.md into every worker as shared behavioral contract 2026-04-22 15:39:25 +02:00
Mathias Bergqvist
f2bc39b500 feat(skills): inject brain context into review, debug, spec, tdd before spawning workers 2026-04-22 15:37:56 +02:00
Mathias Bergqvist
3625e1268d feat(ingestion): simplify brain to knowledge/ — write and search use same dir 2026-04-22 15:36:10 +02:00
Mathias Bergqvist
47df642836 feat(brain): add Query client for skill handler context injection 2026-04-22 15:34:09 +02:00
Mathias Bergqvist
235d70ad0b docs: document hyperguild scope reset — drop parametric learning, simplify brain 2026-04-22 15:27:52 +02:00
17 changed files with 313 additions and 67 deletions

View File

@@ -44,6 +44,29 @@ Record *why* things are the way they are. Future-you will thank present-you.
**Consequences**: More operational complexity than Chroma, but isolation is non-negotiable for client work.
## 2026-04-22 — Hyperguild scope reset: drop parametric learning, simplify brain
**Context**: After shipping Phases 14 (MCP server, 6 skills, model orchestration, session logging, CD pipeline), we critically reviewed what was theater vs genuinely useful.
**Decisions**:
1. **Drop the parametric learning pipeline.** SFT/DPO/RL extraction, `brain/training-data/` directory structure, Axolotl/LLaMA-Factory fine-tuning loop — all cut. The loop requires thousands of high-quality examples to move the needle, which a solo consultant won't generate. Better base models ship faster than any fine-tuning effort could keep up with. This is a research project, not a productivity tool.
2. **Simplify the brain to plain markdown.** `brain/knowledge/` replaces `brain/wiki/ + brain/raw/ + brain/training-data/`. The trainer and retrospective workers write markdown entries. `brain_query` searches markdown. No ingestion pipeline, no tagging for significance review, no structured JSONL formats.
3. **Measure the escalation chain before assuming it's useful.** Local model (phi4) only belongs in a skill's chain if it passes Claude verification at a meaningful rate. Where it fails >70% of the time, it adds cost not value. Per-skill hit rate logging is the prerequisite to honest chain configuration.
4. **Keep what's real**: MCP tool surface, session logging with attempt records, tier detection, CD pipeline, bridge to Claude Code.
**What to build next** (in priority order):
- `brain_query` injection into skill handlers before spawning workers — this makes the declarative brain actually function
- `protocols.md` — behavioral contract injected into every worker prompt
- Per-skill pass rate logging and chain tuning
**Consequences**: Simpler system with a shorter feedback loop. The brain becomes real only when skill handlers query it. Training data ambitions deferred indefinitely — revisit if local model capabilities improve enough that fine-tuning becomes worthwhile.
---
## 2026-04-08 — Mistral Vibe gets its own adapter
**Context**: Vibe doesn't read `AGENTS.md` — it uses `~/.vibe/prompts/` and `~/.vibe/agents/` with TOML config.

View File

@@ -43,6 +43,17 @@ func main() {
os.Exit(1)
}
protocolsPrompt, err := os.ReadFile(cfg.ConfigDir + "/protocols.md")
if err != nil {
logger.Error("read protocols.md", "path", cfg.ConfigDir+"/protocols.md", "err", err)
os.Exit(1)
}
// prependProtocols prepends the shared protocols to a skill discipline file.
prependProtocols := func(skillPrompt []byte) string {
return string(protocolsPrompt) + "\n---\n\n" + string(skillPrompt)
}
tddPrompt, err := os.ReadFile(cfg.ConfigDir + "/tdd.md")
if err != nil {
logger.Error("read tdd.md", "path", cfg.ConfigDir+"/tdd.md", "err", err)
@@ -103,6 +114,18 @@ func main() {
orch := iexec.NewOrchestrator(chain, litellmExec.Run, claudeExec.Run, verifier, models.LlamaSwapURL(), &attempts)
result, err := orch.Run(ctx, req)
result.Attempts = attempts // attach orchestration metadata before returning
// Log per-attempt verdicts so pass rates are visible in pod logs.
for i, a := range attempts {
logger.Info("chain attempt",
"skill", skill,
"attempt", i+1,
"model", a.Model,
"tier", a.Tier,
"verdict", a.Verdict,
"duration_ms", a.DurationMs,
"warm", a.WarmStart,
)
}
return result, err
}
}
@@ -114,10 +137,11 @@ func main() {
reg := registry.New()
reg.Register(tdd.New(tdd.Config{
SystemPrompt: string(systemPrompt),
SkillPrompt: string(tddPrompt),
SkillPrompt: prependProtocols(tddPrompt),
DefaultModel: models.ChainFor("tdd", "")[0],
ExecutorFn: buildOrch("tdd"),
SessionsDir: cfg.SessionsDir,
IngestBaseURL: cfg.IngestBaseURL,
}))
reg.Register(brain.New(brain.Config{
IngestBaseURL: cfg.IngestBaseURL,
@@ -129,32 +153,35 @@ func main() {
SessionsDir: cfg.SessionsDir,
}))
reg.Register(retrospective.New(retrospective.Config{
SkillPrompt: string(retroPrompt),
SkillPrompt: prependProtocols(retroPrompt),
DefaultModel: models.ChainFor("retrospective", "")[0],
SessionsDir: cfg.SessionsDir,
ExecutorFn: buildOrch("retrospective"),
}))
reg.Register(review.New(review.Config{
SkillPrompt: string(reviewPrompt),
SkillPrompt: prependProtocols(reviewPrompt),
DefaultModel: models.ChainFor("review", "")[0],
ExecutorFn: buildOrch("review"),
SessionsDir: cfg.SessionsDir,
IngestBaseURL: cfg.IngestBaseURL,
}))
reg.Register(skilldebug.New(skilldebug.Config{
SkillPrompt: string(debugPrompt),
SkillPrompt: prependProtocols(debugPrompt),
DefaultModel: models.ChainFor("debug", "")[0],
ExecutorFn: buildOrch("debug"),
SessionsDir: cfg.SessionsDir,
IngestBaseURL: cfg.IngestBaseURL,
}))
reg.Register(spec.New(spec.Config{
SkillPrompt: string(specPrompt),
SkillPrompt: prependProtocols(specPrompt),
DefaultModel: models.ChainFor("spec", "")[0],
ExecutorFn: buildOrch("spec"),
SessionsDir: cfg.SessionsDir,
IngestBaseURL: cfg.IngestBaseURL,
}))
reg.Register(trainer.New(trainer.Config{
ReaderPrompt: string(trainerReaderPrompt),
WriterPrompt: string(trainerWriterPrompt),
ReaderPrompt: prependProtocols(trainerReaderPrompt),
WriterPrompt: prependProtocols(trainerWriterPrompt),
DefaultModel: models.ChainFor("trainer", "")[0],
ExecutorFn: buildOrch("trainer"),
SessionsDir: cfg.SessionsDir,

View File

@@ -1,27 +1,55 @@
# The Hyperguild Way
# The Hyperguild Way — Worker Protocols
These protocols are injected into every worker invocation. They define how you behave as a member of the hyperguild.
Injected into every worker invocation alongside the skill discipline file.
Defines the behavioral contract all workers must follow regardless of skill or model.
---
## 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.
Every response is raw JSON matching the skill's output schema.
- No preamble, no explanation, no prose before or after the JSON
- No markdown code fences around the JSON
- Malformed output is treated as a failed invocation
- If uncertain, return valid JSON anyway — put uncertainty in `message`
## Quality gate
`verified: true` only when a subprocess exit code confirms the outcome. Never self-assess. "I think the tests pass" is not verified.
`verified: true` means the artifact was independently confirmed correct:
- For code: the test runner exited 0
- For files: the file exists at the path you wrote
- Never set `verified: true` based on self-assessment — only on external confirmation
- "I think the tests pass" is not verified. Run them.
## 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.
If stuck after a genuine attempt:
- Return `status: error` with a clear reason in `message`
- Do not retry silently or produce low-confidence output labelled as a pass
- Do not hallucinate file paths, test results, or exit codes
- Maximum 3 attempts per phase — if output is identical across two consecutive attempts, stop immediately
## Working offline
## Context usage
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".
You may receive a `## Relevant knowledge` block and/or a `## Session history` block before your task.
- **Relevant knowledge**: patterns, decisions, and conventions from past sessions. Let them inform your approach — do not contradict them without reason.
- **Session history**: what has already happened in this session. Build on it, do not repeat it.
- If either block is absent, proceed with the skill discipline file only. Note the absence in `message` if it materially affects quality.
## Handoff format
## Handoff discipline
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.
Structure output so the next worker in a chain can consume it without transformation:
- `file_path`: absolute path to the primary artifact you produced
- `runner_output`: verbatim stdout+stderr from the last command you ran (truncate to 2000 chars if longer)
- `message`: one sentence — what you did and whether it worked
## Scope
You have access to `Bash`, `Read`, and `Write` only.
- Do not attempt to call MCP tools or make HTTP requests
- Do not modify files outside `project_root` without explicit instruction
- If the task requires capabilities you do not have, return `status: error`
## Session logging
The Go skill handler records your invocation in the session log automatically. You do not need to do this yourself.
The Go skill handler records your invocation automatically. You do not need to log anything yourself.

View File

@@ -79,7 +79,7 @@ func (h *Handler) Write(w http.ResponseWriter, r *http.Request) {
filename = fmt.Sprintf("%s-auto.md", time.Now().UTC().Format("2006-01-02-150405"))
}
rawDir := filepath.Join(h.brainDir, "raw")
rawDir := filepath.Join(h.brainDir, "knowledge")
if err := os.MkdirAll(rawDir, 0o755); err != nil {
http.Error(w, "failed to create raw dir", http.StatusInternalServerError)
return

View File

@@ -20,10 +20,9 @@ import (
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.MkdirAll(filepath.Join(dir, "knowledge"), 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(dir, "wiki", "concepts", "tdd.md"),
filepath.Join(dir, "knowledge", "tdd.md"),
[]byte("---\ntitle: TDD\ndomain: software\n---\n\nTest-driven development is a discipline.\n"),
0o644,
))
@@ -46,7 +45,7 @@ func TestQuery_ReturnsResults(t *testing.T) {
assert.NotEmpty(t, results)
}
func TestWrite_CreatesRawFile(t *testing.T) {
func TestWrite_CreatesKnowledgeFile(t *testing.T) {
dir, h := setup(t)
body, _ := json.Marshal(map[string]any{
"content": "# Test note\n\nSome content.",
@@ -62,8 +61,7 @@ func TestWrite_CreatesRawFile(t *testing.T) {
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)
content, err := os.ReadFile(filepath.Join(dir, "knowledge", "test-note.md"))
require.NoError(t, err)
assert.Contains(t, string(content), "Some content.")
}
@@ -93,7 +91,7 @@ func TestWrite_IncludesFrontmatterWhenTypeProvided(t *testing.T) {
h.Write(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
content, err := os.ReadFile(filepath.Join(dir, "raw", "typed-note.md"))
content, err := os.ReadFile(filepath.Join(dir, "knowledge", "typed-note.md"))
require.NoError(t, err)
assert.Contains(t, string(content), "type: concept")
assert.Contains(t, string(content), "domain: software")
@@ -109,7 +107,8 @@ func TestWrite_GeneratesFilenameIfAbsent(t *testing.T) {
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"))
entries, _ := os.ReadDir(filepath.Join(dir, "knowledge"))
// +1 because setup already wrote tdd.md
assert.Len(t, entries, 2)
assert.True(t, strings.HasSuffix(entries[1].Name(), ".md"))
}

View File

@@ -33,7 +33,7 @@ func Query(brainDir, query string, limit int) ([]Result, error) {
var results []Result
err := filepath.WalkDir(filepath.Join(brainDir, "wiki"), func(path string, d os.DirEntry, err error) error {
err := filepath.WalkDir(filepath.Join(brainDir, "knowledge"), func(path string, d os.DirEntry, err error) error {
if err != nil {
slog.Warn("search: skipping path", "path", path, "err", err)
return nil

View File

@@ -14,17 +14,15 @@ import (
func TestSearch_ReturnsMatchingPages(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "knowledge"), 0o755))
// Write a concept page mentioning "retry"
require.NoError(t, os.WriteFile(
filepath.Join(dir, "wiki", "concepts", "retry-logic.md"),
filepath.Join(dir, "knowledge", "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"),
filepath.Join(dir, "knowledge", "database.md"),
[]byte("---\ntitle: Database\ndomain: software\n---\n\nA database stores structured data.\n"),
0o644,
))
@@ -32,7 +30,7 @@ func TestSearch_ReturnsMatchingPages(t *testing.T) {
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, "knowledge/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")
@@ -40,10 +38,10 @@ func TestSearch_ReturnsMatchingPages(t *testing.T) {
func TestSearch_RespectsLimit(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.MkdirAll(filepath.Join(dir, "wiki", "concepts"), 0o755))
require.NoError(t, os.MkdirAll(filepath.Join(dir, "knowledge"), 0o755))
for i := 0; i < 5; i++ {
require.NoError(t, os.WriteFile(
filepath.Join(dir, "wiki", "concepts", fmt.Sprintf("page-%d.md", i)),
filepath.Join(dir, "knowledge", fmt.Sprintf("page-%d.md", i)),
[]byte(fmt.Sprintf("---\ntitle: Page %d\n---\n\nThis page mentions retry.\n", i)),
0o644,
))

76
internal/brain/client.go Normal file
View File

@@ -0,0 +1,76 @@
// internal/brain/client.go
// Package brain provides a lightweight client for querying the ingestion server.
// Skill handlers call Query before spawning workers to inject relevant knowledge
// from the brain into the task prompt. Errors are suppressed — the brain is
// optional context; its absence must never block a skill invocation.
package brain
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
)
type queryResult struct {
Path string `json:"path"`
Title string `json:"title"`
Excerpt string `json:"excerpt"`
Score int `json:"score"`
}
// Query calls the ingestion server and returns relevant knowledge as a
// formatted string ready to prepend to a worker task prompt.
// Returns empty string (no error) when baseURL or query is empty,
// when the brain is unreachable, or when no results are found.
func Query(ctx context.Context, baseURL, query string, limit int) (string, error) {
if baseURL == "" || strings.TrimSpace(query) == "" {
return "", nil
}
if limit <= 0 {
limit = 3
}
body, _ := json.Marshal(map[string]any{"query": query, "limit": limit})
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/query", bytes.NewReader(body))
if err != nil {
slog.Warn("brain: build request failed", "err", err)
return "", nil
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
slog.Warn("brain: ingestion server unreachable", "err", err)
return "", nil
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
slog.Warn("brain: ingestion server returned non-OK", "status", resp.StatusCode)
return "", nil
}
out, _ := io.ReadAll(resp.Body)
var result struct {
Results []queryResult `json:"results"`
}
if err := json.Unmarshal(out, &result); err != nil || len(result.Results) == 0 {
return "", nil
}
var b strings.Builder
b.WriteString("## Relevant knowledge\n\n")
for _, r := range result.Results {
title := r.Title
if title == "" {
title = r.Path
}
fmt.Fprintf(&b, "### %s\n%s\n\n", title, r.Excerpt)
}
return b.String(), nil
}

View File

@@ -0,0 +1,67 @@
package brain_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/mathiasbq/supervisor/internal/brain"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestQueryEmptyBaseURL(t *testing.T) {
result, err := brain.Query(context.Background(), "", "tdd patterns", 3)
require.NoError(t, err)
assert.Empty(t, result)
}
func TestQueryEmptyQuery(t *testing.T) {
result, err := brain.Query(context.Background(), "http://localhost:9999", "", 3)
require.NoError(t, err)
assert.Empty(t, result)
}
func TestQueryFormatsResults(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/query", r.URL.Path)
var req map[string]any
require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
assert.Equal(t, "tdd patterns", req["query"])
json.NewEncoder(w).Encode(map[string]any{ //nolint:errcheck
"results": []map[string]any{
{"path": "knowledge/tdd.md", "title": "TDD Guide", "excerpt": "Always write tests first.", "score": 5},
{"path": "knowledge/go.md", "title": "Go Conventions", "excerpt": "Use table-driven tests.", "score": 3},
},
})
}))
defer srv.Close()
result, err := brain.Query(context.Background(), srv.URL, "tdd patterns", 3)
require.NoError(t, err)
assert.Contains(t, result, "## Relevant knowledge")
assert.Contains(t, result, "TDD Guide")
assert.Contains(t, result, "Always write tests first.")
assert.Contains(t, result, "Go Conventions")
}
func TestQueryEmptyResults(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"results": []any{}}) //nolint:errcheck
}))
defer srv.Close()
result, err := brain.Query(context.Background(), srv.URL, "obscure query", 3)
require.NoError(t, err)
assert.Empty(t, result)
}
func TestQueryUnavailableServerReturnsEmpty(t *testing.T) {
// Brain unavailable — should degrade gracefully, no error
result, err := brain.Query(context.Background(), "http://127.0.0.1:19999", "query", 3)
require.NoError(t, err)
assert.Empty(t, result)
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"time"
"github.com/mathiasbq/supervisor/internal/brain"
iexec "github.com/mathiasbq/supervisor/internal/exec"
"github.com/mathiasbq/supervisor/internal/session"
)
@@ -40,11 +41,16 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
model = s.cfg.DefaultModel
}
brainCtx, _ := brain.Query(ctx, s.cfg.IngestBaseURL, a.Error+" "+a.Context, 3)
task := fmt.Sprintf(
"phase: debug\nproject_root: %s\nerror: %s\ncontext: %s\nmodel: %s",
a.ProjectRoot, a.Error, a.Context, model,
)
task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "debug", task)
if brainCtx != "" {
task = brainCtx + "\n---\n\n" + task
}
if s.cfg.ExecutorFn == nil {
return nil, fmt.Errorf("no executor configured")

View File

@@ -18,6 +18,7 @@ type Config struct {
DefaultModel string
ExecutorFn ExecutorFn
SessionsDir string
IngestBaseURL string // optional: base URL of ingestion server for brain context
}
// Skill implements the debug MCP tool.

View File

@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/mathiasbq/supervisor/internal/brain"
iexec "github.com/mathiasbq/supervisor/internal/exec"
"github.com/mathiasbq/supervisor/internal/session"
)
@@ -41,11 +42,16 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
model = s.cfg.DefaultModel
}
brainCtx, _ := brain.Query(ctx, s.cfg.IngestBaseURL, strings.Join(a.Files, " ")+" "+a.Context, 3)
task := fmt.Sprintf(
"phase: review\nproject_root: %s\nfiles: %s\ncontext: %s\nmodel: %s",
a.ProjectRoot, strings.Join(a.Files, ", "), a.Context, model,
)
task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "review", task)
if brainCtx != "" {
task = brainCtx + "\n---\n\n" + task
}
if s.cfg.ExecutorFn == nil {
return nil, fmt.Errorf("no executor configured")

View File

@@ -18,6 +18,7 @@ type Config struct {
DefaultModel string
ExecutorFn ExecutorFn
SessionsDir string
IngestBaseURL string // optional: base URL of ingestion server for brain context
}
// Skill implements the review MCP tool.

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"time"
"github.com/mathiasbq/supervisor/internal/brain"
iexec "github.com/mathiasbq/supervisor/internal/exec"
"github.com/mathiasbq/supervisor/internal/session"
)
@@ -45,11 +46,16 @@ func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (
model = s.cfg.DefaultModel
}
brainCtx, _ := brain.Query(ctx, s.cfg.IngestBaseURL, a.Requirements+" "+a.Context, 3)
task := fmt.Sprintf(
"phase: spec\nproject_root: %s\nrequirements: %s\noutput_path: %s\ncontext: %s\nmodel: %s",
a.ProjectRoot, a.Requirements, outputPath, a.Context, model,
)
task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "spec", task)
if brainCtx != "" {
task = brainCtx + "\n---\n\n" + task
}
if s.cfg.ExecutorFn == nil {
return nil, fmt.Errorf("no executor configured")

View File

@@ -18,6 +18,7 @@ type Config struct {
DefaultModel string
ExecutorFn ExecutorFn
SessionsDir string
IngestBaseURL string // optional: base URL of ingestion server for brain context
}
// Skill implements the spec MCP tool.

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"time"
"github.com/mathiasbq/supervisor/internal/brain"
iexec "github.com/mathiasbq/supervisor/internal/exec"
"github.com/mathiasbq/supervisor/internal/session"
)
@@ -41,10 +42,15 @@ func (s *Skill) handleRed(ctx context.Context, raw json.RawMessage) (json.RawMes
if args.Spec == "" {
return nil, fmt.Errorf("spec is required")
}
brainCtx, _ := brain.Query(ctx, s.cfg.IngestBaseURL, args.Spec, 3)
task := fmt.Sprintf(
"phase: red\nproject_root: %s\nspec: %s\nmodel: %s\ntest_cmd: %s",
args.ProjectRoot, args.Spec, s.resolveModel(args.Model), args.TestCmd,
)
if brainCtx != "" {
task = brainCtx + "\n---\n\n" + task
}
return s.execute(ctx, task)
}

View File

@@ -17,6 +17,7 @@ type Config struct {
ExecutorFn ExecutorFn // nil = no executor (tests that don't reach execute())
DefaultModel string
SessionsDir string // optional: path to brain/sessions/ for history injection
IngestBaseURL string // optional: base URL of ingestion server for brain context
}
type Skill struct {