PR #131 removed WriteNote's legacy brain/knowledge/ fallback branch that
activated when wing/hall were both empty, but never updated the CLI that
was the only caller relying on it. Since `hyperguild brain write` never
sent wing/hall, every invocation of the CLI's most load-bearing brain
command started hard-erroring "wing and hall are required" — a live,
README-documented command broken on every call.
Separately, writeRequest never had a `slug` field (pre-existing, unrelated
to #131): the CLI's slug value was silently dropped server-side even
before this PR, falling back to a timestamp filename.
Fix:
- `hyperguild brain write <wing> <hall> <slug>` replaces the old
`<type> <slug>` shape. Positional args, mirroring the existing
`brain query <topic>` pattern and matching writeHallNote's own
wing/hall/slug parameter order server-side (handler.go's
brain.NotePath(brainDir, wing, hall, slug)) — no new flags needed since
wing/hall/slug are all required, not optional filters.
- brainClient.Write now sends {wing, hall, filename, content}, matching
writeRequest exactly (writeRequest already gained a Filename field in
#131, so no server-side change was needed — only the CLI was sending
the wrong shape). This also fixes the dead slug bug: slug now arrives
server-side as filename instead of being silently dropped.
- Added TestWrite_CLIPayloadRoundTrips in ingestion/internal/api, which
POSTs the CLI's exact JSON envelope to the real Handler.Write (not a
mock) and asserts a real file with real wing/hall frontmatter lands on
disk. cmd/hyperguild and ingestion are separate Go modules (no
go.work), so this can't be a single cross-module test — instead the
wire contract is pinned from the server side, closing the gap where
cmd/hyperguild/http_test.go's TestBrainClient_Write_Success used only a
hand-rolled httptest mock and stayed green through the wing/hall
regression.
- Updated cmd/hyperguild/README.md and the 2026-05-03 CLI design spec's
stale `<type> <slug>` examples.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
The BrainStore is not the only door. POST /write — the HTTP path CLAUDE.md
hands to curl and scripts — calls api.WriteNote directly, so the strip
shipped one commit ago covered brain_write and capture while leaving the raw
door ungated. A gate one door wide is not a gate.
The BrainStore keeps its own call so it can still report Ref.Unlinked to the
MCP caller; running the strip twice is a no-op on content the primitive has
already cleaned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
First half of brain#21: half_life (volatile|seasonal|durable), applies
(local|homelab|universal), claim_provenance (asserted|inferred).
writeHallNote already passed unknown frontmatter keys through verbatim, so
preservation was free — a test now pins it so a refactor of the merge loop
cannot silently drop the fields. What was missing is validation, and it
fails closed: a value outside the enum is rejected (a lint skips what it
cannot parse, so the note would read as classified while nothing acts on
it); an envelope field without claim_provenance is rejected (a value a
model inferred must never be indistinguishable from one a human asserted,
or a single classifier pass launders guesses into apparent judgement
across the corpus); provenance alone is rejected too.
Validation runs before MkdirAll, so a rejected write leaves nothing behind.
The field is `applies`, not `scope`: scope: has been in use since brain#5
for glossary terms (homelab-core, project:<repo>), a disjoint value space
on the same key. The shipped schema keeps the name.
Notes with none of these fields are byte-identical to before. Bulk
backfill stays out of scope.
Schema documented in brain/CLAUDE.md via brain_edit_root, which produced
the hyperguild#97 audit record for the rewrite.
Refs brain#21, brain#19 (superseded), brain#5, hyperguild#97.
Co-authored-by: Mathias <mthbqv@gmail.com>
opts.Content can already carry its own "---\n...\n---" block (e.g. from
an upstream extraction/promotion step). writeHallNote used to prepend a
second block unconditionally, making a standard frontmatter parser blind
to everything the first block declared (title, tags, ...).
splitFrontmatter now pulls the existing block's fields out first;
wing/hall/created_at are always fresh-injected, and type/domain/
source_type prefer the note's own existing value over the opts
fallback. Remaining existing fields are carried through verbatim into
the single merged block.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Roq1ajWKR5f1hG5Df9wC6A
The unsourced-check fix (source_type: internal) only covered
claudewatcher's writes. 41 residual findings on the audit's next run
were manually-authored hall=facts entries written via brain_write/
capture -- also first-party observations, just a different write path,
with no way to mark them internal short of hand-editing every entry.
writeHallNote now defaults source_type to internal whenever hall ==
"facts" and the caller didn't set it explicitly. An explicit
source_type: external (now threaded through the /write HTTP endpoint)
opts a genuinely citation-needing entry back out.
brain-gardener#5: the audit's unsourced check was flagging 59 raw
claudewatcher session dumps as high-severity hallucination risk,
because it can't distinguish a first-party observation (a session
transcript) from an external claim needing citation.
Adds WriteNoteOptions.SourceType, emitted as source_type: <value> in
the wing/hall frontmatter route. claudeSink (the claudewatcher sink)
now always sets source_type: internal on the notes it writes.
Allows callers to provide pre-structured RawPage data directly, bypassing the
LLM extraction step. The pipeline still handles slug computation, frontmatter,
link canonicalization, source back-references, and dedup — only the extraction
is skipped. Useful when a more capable model or manual curation produces the
structured data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires pipeline.Run into the HTTP layer so callers can ingest raw text
or files/directories without touching the filesystem directly. Rewrites
main.go to parse LLM and watcher env vars and build pipeline.Config.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empty or whitespace-only queries would silently pass through to search,
returning meaningless results. Also removed the Domain field from
queryRequest — it was accepted but silently ignored since search.Query
has no domain parameter, which would confuse callers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements POST /query (BM25 search via internal/search) and POST /write
(raw file persistence to brain/raw/) as an api.Handler struct. Filename
is auto-generated when absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>