chore(brain): remove brain_write legacy params domain/type/knowledge-path (ADR-0012)
CI / Lint / Test / Vet (pull_request) Successful in 33s
CI / Mirror to GitHub (pull_request) Skipped

hyperguild#79's caller audit (done in a prior session) found the removal
was safe:

- The promote/pending flow (#38/#81, ingestion/internal/api/promote.go)
  operates exclusively on brainDir/raw/ -> brainDir/wiki/<wing>/<hall>/. It
  never touches brainDir/knowledge/, so removing the knowledge/ write path
  does not break it.
- No live caller uses domain/type or the knowledge/ path: checked
  hyperguild itself, mathias/skills, mathias/agentsquad, mathias/dispatch,
  mathias/cad, mathias/cad-atlas.
- internal/skills/brain (a skill.go + handlers.go pair) defined a third,
  even-more-legacy brain_write shape (type/domain/filename, no wing/hall)
  for an old "supervisor" MCP server whose cmd/supervisor/main.go no
  longer exists. Nothing outside its own test imported it; deleted
  entirely as it encodes the exact legacy shape this issue removes.

Changes:
- ingestion/internal/mcp/handlers.go: brain_write's tool schema and
  brainWriteArgs drop type/domain; wing+hall are now marked required in
  the schema.
- ingestion/internal/api/handler.go: WriteNote has one behaviour — wing+
  hall required, routing to wiki/<wing>/<hall>/. writeLegacyNote and the
  knowledge/-path branch are deleted. Omitting wing or hall now returns
  "wing and hall are required" (400 over HTTP, a tool error over MCP) —
  the only sane failure mode left once there is no other valid
  destination.
- ingestion/internal/capture/ports.go, internal/brainstore/store.go: drop
  Type/Domain from capture.Note and its WriteNoteOptions construction.
  pipeline/*.go's unrelated "domain" field (a different, legitimate
  concept for brain_ingest_raw) is untouched.
- internal/skills/brain/: deleted (orphaned legacy tool schema, dead
  since cmd/supervisor was removed).

Tests: added TestWriteNote_RequiresWingAndHall (api),
TestBrainWriteRequiresWingAndHall +
TestBrainWriteIgnoresLegacyTypeAndDomainParams (mcp) for the new single
behaviour; removed tests that only exercised the deleted legacy path
(TestWrite_IncludesFrontmatterWhenTypeProvided,
TestWriteNote_HallRouteExistingTypeWinsOverOptsType,
TestBrainWriteRejectsTraversal, TestBrainWriteAcceptsDoubleDotInName —
the latter two's filename-sanitisation behaviour is covered independently
by internal/brain/path_test.go's TestNotePath_SanitisesWingAndSlug /
TestSanitise, which is unaffected by this change).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 07:32:27 +02:00
co-authored by Claude Sonnet 5
parent cdafdf6e0f
commit e8e233823d
9 changed files with 108 additions and 578 deletions
+4 -10
View File
@@ -53,14 +53,12 @@ func (s *Server) tools() []map[string]any {
},
{
"name": "brain_write",
"description": "Write a markdown note to the brain. With wing+hall set, routes to brain/wiki/<wing>/<hall>/ with wing/hall/created_at frontmatter; otherwise writes to brain/knowledge/ (legacy).",
"inputSchema": schema([]string{"content"}, map[string]any{
"description": "Write a markdown note to brain/wiki/<wing>/<hall>/ with wing/hall/created_at frontmatter. Both wing and hall are required.",
"inputSchema": schema([]string{"content", "wing", "hall"}, map[string]any{
"content": str("markdown content"),
"filename": str("optional filename or slug"),
"type": str("optional frontmatter type (legacy)"),
"domain": str("optional frontmatter domain (legacy)"),
"wing": str("optional topic domain, e.g. jepa-fx"),
"hall": enum("optional memory type (requires wing)", halls...),
"wing": str("topic domain, e.g. jepa-fx"),
"hall": enum("memory type", halls...),
}),
},
{
@@ -227,8 +225,6 @@ func (s *Server) brainQuery(ctx context.Context, args json.RawMessage) (json.Raw
type brainWriteArgs struct {
Content string `json:"content"`
Filename string `json:"filename,omitempty"`
Type string `json:"type,omitempty"`
Domain string `json:"domain,omitempty"`
Wing string `json:"wing,omitempty"`
Hall string `json:"hall,omitempty"`
}
@@ -254,8 +250,6 @@ func (s *Server) brainWrite(ctx context.Context, args json.RawMessage) (json.Raw
ref, err := s.store.Write(ctx, capture.Note{
Content: a.Content,
Filename: a.Filename,
Type: a.Type,
Domain: a.Domain,
Wing: a.Wing,
Hall: a.Hall,
})
+35 -29
View File
@@ -51,25 +51,50 @@ func TestBrainQueryReturnsResults(t *testing.T) {
assert.Contains(t, text, "tdd.md")
}
func TestBrainWriteCreatesFile(t *testing.T) {
// TestBrainWriteIgnoresLegacyTypeAndDomainParams pins the post-#79 contract:
// domain/type were removed from the schema and the handler's arg struct, so
// a caller that still sends them (the old MCP surface) gets a normal wing+
// hall write with no error and no effect from the stale fields — silently
// dropped by json.Unmarshal, not rejected. Only content in its OWN
// frontmatter (unrelated to these params) can set type/domain, per
// TestWriteNote_HallRouteMergesExistingFrontmatterInsteadOfStacking in the
// api package.
func TestBrainWriteIgnoresLegacyTypeAndDomainParams(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "# Test\n\nbody",
"filename": "test.md",
"filename": "test",
"wing": "homelab",
"hall": "facts",
"type": "note",
"domain": "personal",
})
require.Nil(t, resp["error"])
got, err := os.ReadFile(filepath.Join(brainDir, "knowledge", "test.md"))
got, err := os.ReadFile(filepath.Join(brainDir, "wiki", "homelab", "facts", "test.md"))
require.NoError(t, err)
assert.Contains(t, string(got), "type: note")
assert.Contains(t, string(got), "domain: personal")
assert.NotContains(t, string(got), "type: note")
assert.NotContains(t, string(got), "domain: personal")
assert.Contains(t, string(got), "# Test")
}
// TestBrainWriteRequiresWingAndHall is the MCP-tool-call counterpart of
// api.TestWriteNote_RequiresWingAndHall: the legacy brain/knowledge/
// fallback is gone, so a caller omitting either param gets a tool error,
// not a silent legacy write.
func TestBrainWriteRequiresWingAndHall(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "x",
"filename": "no-destination",
})
require.NotNil(t, resp["error"])
}
func TestBrainWriteWingHallRoutesToWiki(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
@@ -208,30 +233,11 @@ func TestBrainTunnelRejectsMissing(t *testing.T) {
require.NotNil(t, resp["error"])
}
func TestBrainWriteRejectsTraversal(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "x",
"filename": "../escape.md",
})
require.NotNil(t, resp["error"])
}
func TestBrainWriteAcceptsDoubleDotInName(t *testing.T) {
brainDir := t.TempDir()
srv := mcp.NewServer(brainDir, nil, nil, nil)
resp := toolCall(t, srv, "brain_write", map[string]any{
"content": "x",
"filename": "notes..draft.md",
})
require.Nil(t, resp["error"])
_, err := os.Stat(filepath.Join(brainDir, "knowledge", "notes..draft.md"))
require.NoError(t, err, "filename with embedded .. should be allowed")
}
// Filename traversal/sanitisation on the wiki route is covered directly
// against brain.NotePath/Sanitise in internal/brain/path_test.go
// (TestNotePath_SanitisesWingAndSlug, TestSanitise) — those tests exercised
// the now-deleted brain/knowledge/ legacy path's own separate filename
// validation, which had no equivalent once that path was removed (#79).
func TestBrainIngestRawDryRun(t *testing.T) {
brainDir := t.TempDir()