Review round 1 on PR #189 (three tests stayed green with their guard
removed):
- TestEditRefusesANoteBehindASymlinkedDirectory had no .git, so Edit
refused "not the sync source" before any path check. It now has one,
plus a positive control.
- AppendRegular's unclean-path case was refused by a final symlink, not
cleanRel. New test where ".." after a symlinked dir resolves to a real
file, for AppendRegular and PutRegular.
- brainfs.Remove had no test; new one (refuses behind a symlinked dir,
removes a final link but never its target, positive control).
Also: promote/reject create with O_EXCL (new brainfs.CreateRegular), so a
note that appears after the collision check is never overwritten;
gofmt fixes in provenance.go and wiki/log.go; dead wingDir and
putBrainFile removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Writers that created or rewrote brain files by path followed symlinks:
- promote/reject: os.MkdirAll + os.WriteFile of the destination, then
os.Remove of the raw source. Now PutRegular + brainfs.Remove; the source
is kept when the write is refused.
- StampProvenanceFile (brain_edit's re-stamp): provenance.ApplyFile read
and rewrote by path. Now ReadRegular + WriteRegular; ApplyFile, now
unused, is removed.
- BuildWingIndex (_index.md) and wiki.RebuildIndex (wiki/index.md): now
PutRegular.
- wiki.AppendLog (log.md) and AutoTunnel's fuzzy-candidate log
(raw/tunnel-candidates-*.md): now AppendRegular; tunnel's fileExists,
now unused, is removed.
- New brainfs.Remove: clean path, real parent, removes a final symlink
itself, never its target.
brain_edit's own atomic rename was already refused behind a symlinked
directory by #187's read check; a guard test pins it.
Pipeline writes, BackfillRefs, the watcher's raw/ markers and the session
archive in cmd/server follow in a separate PR (#158).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DetectTunnels (inside every brain_write/brain_update via AutoTunnel) walks with brainfs.WalkRegular and reads titles with ReadRegular: a FIFO under wiki/ no longer hangs the write, and a symlinked note's title no longer becomes a tunnel candidate. A wiki/ that is not a real directory yields no candidates; other walk errors still abort.
Fixes#174. Refs #157 (last reader slice). Adversarial review: BLOCKED, PASS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
BuildWingIndex/BuildAllWingIndexes list wings and halls with the new brainfs.ReadDirDirs and ReadDirRegular and read titles with ReadRegular: a symlinked note no longer reaches _index.md, a symlinked wing is refused (nothing written into its target), a symlinked hall is skipped. linkcheck entryIndex walks with WalkRegular, so a symlinked .md cannot make a link resolve, and a linked tree that is not a real directory is skipped with a warning (brain_write still lands).
Refs #157. Adversarial review: BLOCKED, PASS. Follow-up: #178.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Mathias <mthbqv@gmail.com>
Adds the `brain_tunnel` MCP tool and auto-tunnel behaviour for
`brain_write`, so concepts that appear in multiple wings become
navigable from any of them.
New surface in package brain:
- WriteTunnel(brainDir, src, tgt) — appends a `## See also` bidirectional
wikilink between two notes in different wings. Idempotent (link not
duplicated on re-call) and reuses an existing See also section.
- DetectTunnels(brainDir, content) — walks brain/wiki/, returns
TunnelCandidates for notes whose title appears in content. Tags
whole-word case-insensitive hits as Exact=true and substring-only hits
as Exact=false.
- AutoTunnel(brainDir, src, content) — wraps DetectTunnels: writes
cross-wing exact matches, stages fuzzy matches into
brain/raw/tunnel-candidates-<YYYY-MM-DD>.md for human review.
MCP wiring:
- `brain_tunnel` tool: explicit manual link (source, target).
- `brain_write` with wing+hall now triggers AutoTunnel on the new
content. Failures are logged and never abort the primary write.
readTitleAndCreated also humanises the slug fallback (hyphens → spaces)
so titleless notes participate in content matching.
Closes hyperguild#16.
Tests: idempotency, same-wing rejection, missing-note rejection,
See-also reuse, exact/fuzzy detection, slug fallback, MCP tool happy
path, auto-tunnel hook (cross-wing exact → linked; same-wing → skipped;
fuzzy → candidates file).
Adds a two-dimensional address (wing, hall) to brain notes. A wing is a
topic domain (e.g. jepa-fx, hyperguild); a hall is one of a closed
vocabulary of memory types (facts, decisions, failures, hypotheses,
sources). Notes route to brain/wiki/<wing>/<hall>/<slug>.md with
wing/hall/created_at YAML frontmatter, making the directory a valid
Obsidian vault.
Changes:
- new package ingestion/internal/brain (NotePath, ValidHalls, Sanitise,
BuildWingIndex, BuildAllWingIndexes)
- api.WriteNote refactored to WriteNoteOptions; wing+hall routes to
brain/wiki/, otherwise falls back to brain/knowledge/ (legacy)
- search.Query → QueryOptions with optional Wing/Hall filtering; Result
carries wing/hall extracted from frontmatter or path segments
- MCP tools brain_write and brain_query gain optional wing/hall params
(hall enum-validated); new brain_index tool regenerates _index.md MOC
- POST /index REST endpoint mirrors brain_index
- brain_write auto-rebuilds the wing's _index.md after a wing+hall write
- scripts/migrate-brain-halls.sh migrates flat brain/wiki/{concepts,entities}/
into the new layout (dry-run by default, --commit applies)
All existing tests pass; new tests cover wing/hall write routing, scope
filtering, invalid hall rejection, _index.md generation, and migration
script paths.
Closes hyperguild#1.