Compare commits
6 Commits
7d5289ac54
...
823de23213
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
823de23213 | ||
|
|
78d3939caa | ||
|
|
f2bc39b500 | ||
|
|
3625e1268d | ||
|
|
47df642836 | ||
|
|
235d70ad0b |
23
DECISIONS.md
23
DECISIONS.md
@@ -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 1–4 (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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
76
internal/brain/client.go
Normal 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
|
||||
}
|
||||
67
internal/brain/client_test.go
Normal file
67
internal/brain/client_test.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user