Implements #45 — the keystone supersede-by-slug write verb plus the read-after-write handle on the create path. Decisions in the issue were taken as settled; nothing relitigated.
What landed
brain_update — supersede-by-slug, whole-note body replace. Resolves target from wing/hall/slug (or a full path in slug/path). Missing target → clear error, no silent create. Rewrites frontmatter: stamps updated_at, supersedes (prior content hash), supersede_reason; preserves created_at/wing/hall/custom fields. Rebuilds the wing _index.md and re-tunnels cross-wing matches (idempotent, best-effort), re-indexes the graph. Returns {id, path, content_hash, superseded}.
brain_get — fetch by id or path (read-only). Returns {id, path, content_hash, frontmatter, body}.
All three registration sites updated: tools() descriptors, handleCall dispatch, server.go package doc.
id / content_hash choice
id = the brain-relative slash path — the de-facto handle today. brain_get(id) and brain_get(path) resolve identically, and the id returned by write/update round-trips through brain_get (covered by tests).
content_hash = lowercase hex sha256 of the full file bytes on disk after the write. brain_get recomputes from the file → identical hash. brain_update hashes the prior file before rewrite and stamps it as supersedes.
Step 0 — embedding re-index (the load-bearing finding)
No write handler re-embeds in-handler — not even brain_write. The MCP Server holds only the read side (s.vector VectorSearcher + s.embedder query embedder); it has no vectorstore.Store with Upsert/Delete. Embeddings are refreshed exclusively out-of-band by:
vectorstore.StartSync — background ticker (cmd/server/main.go:301, default 5 min). mtime-driven: re-embeds any file whose mtime is newer than its oldest chunk's updated_at, deleting stale chunks first.
POST /backfill-embeddings — manual full sync.
So "re-sync on update" = ensure the out-of-band Sync re-embeds the changed file, exactly as the dispatch anticipated. brain_update rewrites the note via os.WriteFile, advancing its mtime past the stored chunks' updated_at, so the next Sync pass re-embeds the new body and drops the stale chunk. I deliberately did not invent an in-handler embed call — that would fork a second mechanism diverging from brain_write.
This is satisfiable, not a blocker — TestBrainUpdateReembedsNewContent drives the realvectorstore.Sync after an update and asserts the new body is re-embedded and the old chunk deleted (no stub of the re-index path).
Design note for review: semantic (vector) read-after-write has a ≤5 min window — the supersede is visible to BM25/lexical immediately and to brain_get synchronously, but pgvector retrieval lags until the next Sync tick. If synchronous semantic refresh is ever needed, the path is to wire a vectorstore.Store into the MCP Server and embed in-handler — a follow-up, out of scope for #45.
Acceptance criteria
brain_update supersedes: body replaced, updated_at/supersedes stamped, prior fields preserved
brain_update on non-existent target → clear error, no note created
Embedding re-synced after update (via mtime-driven Sync); query reflects new content — proven by a real Sync test
brain_update returns {id, path, content_hash}
brain_write return contract extended to {id, path, content_hash}
brain_get(id|path) returns the landed note (frontmatter + body)
Prior version recoverable from git (no sidecar/tombstone)
task check passes
Both tools registered in server.go (all three sites)
Tests: update happy path, update non-existent, embedding-reflects-new-content, brain_get by id, brain_get by path, brain_write handle, frontmatter editor, traversal guards
Notes
One unrelated commit (chore(context): re-sync derived adapters) — task check's context gate flagged pre-existing drift from the root AGENT.md rule-0 update; committing the regen is the documented remedy. Not a content change in this repo.
Not auto-merged, per dispatch — review + merge from claude.ai.
Implements #45 — the keystone supersede-by-slug write verb plus the read-after-write handle on the create path. Decisions in the issue were taken as settled; nothing relitigated.
## What landed
**`brain_update`** — supersede-by-slug, whole-note body replace. Resolves target from `wing`/`hall`/`slug` (or a full path in `slug`/`path`). Missing target → clear error, **no silent create**. Rewrites frontmatter: stamps `updated_at`, `supersedes` (prior content hash), `supersede_reason`; preserves `created_at`/`wing`/`hall`/custom fields. Rebuilds the wing `_index.md` and re-tunnels cross-wing matches (idempotent, best-effort), re-indexes the graph. Returns `{id, path, content_hash, superseded}`.
**`brain_get`** — fetch by `id` or `path` (read-only). Returns `{id, path, content_hash, frontmatter, body}`.
**`brain_write`** — return contract extended `{path}` → `{id, path, content_hash}`. `path` kept for backward compat.
All three registration sites updated: `tools()` descriptors, `handleCall` dispatch, `server.go` package doc.
## `id` / `content_hash` choice
- **`id` = the brain-relative slash path** — the de-facto handle today. `brain_get(id)` and `brain_get(path)` resolve identically, and the `id` returned by write/update round-trips through `brain_get` (covered by tests).
- **`content_hash` = lowercase hex sha256 of the full file bytes on disk** after the write. `brain_get` recomputes from the file → identical hash. `brain_update` hashes the prior file before rewrite and stamps it as `supersedes`.
## Step 0 — embedding re-index (the load-bearing finding)
**No write handler re-embeds in-handler — not even `brain_write`.** The MCP `Server` holds only the read side (`s.vector` VectorSearcher + `s.embedder` query embedder); it has no `vectorstore.Store` with `Upsert`/`Delete`. Embeddings are refreshed exclusively out-of-band by:
1. `vectorstore.StartSync` — background ticker (`cmd/server/main.go:301`, default 5 min). **mtime-driven**: re-embeds any file whose mtime is newer than its oldest chunk's `updated_at`, deleting stale chunks first.
2. `POST /backfill-embeddings` — manual full sync.
So "re-sync on update" = **ensure the out-of-band Sync re-embeds the changed file**, exactly as the dispatch anticipated. `brain_update` rewrites the note via `os.WriteFile`, advancing its mtime past the stored chunks' `updated_at`, so the next Sync pass re-embeds the new body and drops the stale chunk. I deliberately did **not** invent an in-handler embed call — that would fork a second mechanism diverging from `brain_write`.
**This is satisfiable, not a blocker** — `TestBrainUpdateReembedsNewContent` drives the *real* `vectorstore.Sync` after an update and asserts the new body is re-embedded and the old chunk deleted (no stub of the re-index path).
**Design note for review:** semantic (vector) read-after-write has a ≤5 min window — the supersede is visible to BM25/lexical immediately and to `brain_get` synchronously, but pgvector retrieval lags until the next Sync tick. If synchronous semantic refresh is ever needed, the path is to wire a `vectorstore.Store` into the MCP `Server` and embed in-handler — a follow-up, out of scope for #45.
## Acceptance criteria
- [x] `brain_update` supersedes: body replaced, `updated_at`/`supersedes` stamped, prior fields preserved
- [x] `brain_update` on non-existent target → clear error, no note created
- [x] Embedding re-synced after update (via mtime-driven Sync); query reflects new content — proven by a real Sync test
- [x] `brain_update` returns `{id, path, content_hash}`
- [x] `brain_write` return contract extended to `{id, path, content_hash}`
- [x] `brain_get(id|path)` returns the landed note (frontmatter + body)
- [x] Prior version recoverable from git (no sidecar/tombstone)
- [x] `task check` passes
- [x] Both tools registered in `server.go` (all three sites)
- [x] Tests: update happy path, update non-existent, embedding-reflects-new-content, brain_get by id, brain_get by path, brain_write handle, frontmatter editor, traversal guards
## Notes
- One unrelated commit (`chore(context): re-sync derived adapters`) — `task check`'s context gate flagged pre-existing drift from the root `AGENT.md` rule-0 update; committing the regen is the documented remedy. Not a content change in this repo.
- Not auto-merged, per dispatch — review + merge from claude.ai.
Closes #45
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Implements the api-layer half of #45. UpdateNote supersedes a note in
place (whole-note body replace, frontmatter re-stamp: updated_at,
supersedes=prior content hash, supersede_reason), preserving created_at,
wing, hall, and any custom fields. Never creates — a missing target is
an error so callers fall back to brain_write. ReadNote is the read-after-
write primitive (frontmatter + body + content_hash). ContentHash is the
sha256 handle that round-trips write/update → get.
Frontmatter is edited via a line-preserving ordered editor rather than a
yaml.v3 round-trip, which would reorder keys and strip comments — the
brain writes flat key:value frontmatter by hand.
Embeddings are not refreshed here: the rewritten file's mtime advances,
which the out-of-band vectorstore.Sync ticker uses to re-embed it — the
same mechanism brain_write relies on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wires the #45 verbs into the MCP surface (all three sites: tools()
descriptors, handleCall dispatch, package doc comment).
- brain_update: supersede-by-slug or full path; rebuilds wing _index and
re-tunnels cross-wing matches against the new body (idempotent,
best-effort), re-indexes the graph, returns {id, path, content_hash,
superseded}.
- brain_get: fetch by id or path (both are the brain-relative handle);
returns {id, path, content_hash, frontmatter, body}.
- brain_write: return contract extended from {path} to {id, path,
content_hash} — path kept for backward compat — so the create path
also yields a stable handle.
id == relPath; content_hash == sha256 of the file bytes. Tests cover the
supersede happy path, missing-target error + no-create, get by id/path,
write handle, and an end-to-end re-embed test that drives the real
vectorstore.Sync re-index after an update.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
context-sync regenerated the adapters from the updated root AGENT.md
(rule 0 pre-task ritual + TDD constraint). The committed adapters had
drifted; this is the documented `task check` remedy, not a content
change in this repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Traced how a written note's embeddings get (re)built before writing any re-sync code, as instructed.
Finding: there is no in-handler embedding re-index anywhere — not in brain_update, not in brain_write, not in brain_ingest. The MCP Server only holds the read side: s.vector (search.VectorSearcher) and s.embedder (query-time search.Embedder). It has no vectorstore.Store (the Upsert/Delete interface), so it cannot embed in-handler without new wiring.
Embeddings are refreshed strictly out-of-band, two ways:
vectorstore.StartSync — background ticker, wired at ingestion/cmd/server/main.go:301, default 5 min. It is mtime-driven (vectorstore/sync.go): for each file it compares the file mtime against the oldestupdated_at among that file's chunk rows; if the file is newer it deletes all old chunks and re-embeds from scratch (handles shrink/orphan rows cleanly).
POST /backfill-embeddings — synchronous full sync over wiki/ + knowledge/.
Consequence for the contract: "re-sync on update" means ensure the out-of-band Sync re-embeds the changed file — exactly the alternative the dispatch flagged. brain_update rewrites the note with os.WriteFile, so its mtime advances past the stored chunks' updated_at, and the next Sync pass re-embeds the new body + drops the stale chunk. This matches brain_write precisely. I did not add a divergent in-handler embed call.
Not a blocker. The acceptance criterion "a query reflects the new content, not the old" is verified against the real mechanism in TestBrainUpdateReembedsNewContent: write → Sync (embeds OLD) → brain_update → Sync (re-embeds NEW, deletes old chunk), asserted with a recording embedder. No stub of the re-index path.
One design question for you (not blocking #45): semantic/vector retrieval has a ≤5 min staleness window after a supersede (BM25 + brain_get are immediate). If the session-retro skill needs vector read-after-write within seconds, the fix is to wire a vectorstore.Store into the MCP Server and embed the single changed file in-handler. I scoped that out as a follow-up — flagging per your "stop and raise it before building the rest" instruction. Everything here works against the existing architecture as-is.
## Step 0 — embedding re-index trace (do-this-first finding)
Traced how a written note's embeddings get (re)built **before** writing any re-sync code, as instructed.
**Finding: there is no in-handler embedding re-index anywhere — not in `brain_update`, not in `brain_write`, not in `brain_ingest`.** The MCP `Server` only holds the read side: `s.vector` (`search.VectorSearcher`) and `s.embedder` (query-time `search.Embedder`). It has no `vectorstore.Store` (the `Upsert`/`Delete` interface), so it *cannot* embed in-handler without new wiring.
Embeddings are refreshed strictly out-of-band, two ways:
1. **`vectorstore.StartSync`** — background ticker, wired at `ingestion/cmd/server/main.go:301`, default 5 min. It is **mtime-driven** (`vectorstore/sync.go`): for each file it compares the file mtime against the *oldest* `updated_at` among that file's chunk rows; if the file is newer it deletes all old chunks and re-embeds from scratch (handles shrink/orphan rows cleanly).
2. **`POST /backfill-embeddings`** — synchronous full sync over `wiki/` + `knowledge/`.
**Consequence for the contract:** "re-sync on update" means *ensure the out-of-band Sync re-embeds the changed file* — exactly the alternative the dispatch flagged. `brain_update` rewrites the note with `os.WriteFile`, so its mtime advances past the stored chunks' `updated_at`, and the next Sync pass re-embeds the new body + drops the stale chunk. This matches `brain_write` precisely. I did **not** add a divergent in-handler embed call.
**Not a blocker.** The acceptance criterion "a query reflects the new content, not the old" is verified against the real mechanism in `TestBrainUpdateReembedsNewContent`: write → `Sync` (embeds OLD) → `brain_update` → `Sync` (re-embeds NEW, deletes old chunk), asserted with a recording embedder. No stub of the re-index path.
**One design question for you** (not blocking #45): semantic/vector retrieval has a ≤5 min staleness window after a supersede (BM25 + `brain_get` are immediate). If the session-retro skill needs *vector* read-after-write within seconds, the fix is to wire a `vectorstore.Store` into the MCP `Server` and embed the single changed file in-handler. I scoped that out as a follow-up — flagging per your "stop and raise it before building the rest" instruction. Everything here works against the existing architecture as-is.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements #45 — the keystone supersede-by-slug write verb plus the read-after-write handle on the create path. Decisions in the issue were taken as settled; nothing relitigated.
What landed
brain_update— supersede-by-slug, whole-note body replace. Resolves target fromwing/hall/slug(or a full path inslug/path). Missing target → clear error, no silent create. Rewrites frontmatter: stampsupdated_at,supersedes(prior content hash),supersede_reason; preservescreated_at/wing/hall/custom fields. Rebuilds the wing_index.mdand re-tunnels cross-wing matches (idempotent, best-effort), re-indexes the graph. Returns{id, path, content_hash, superseded}.brain_get— fetch byidorpath(read-only). Returns{id, path, content_hash, frontmatter, body}.brain_write— return contract extended{path}→{id, path, content_hash}.pathkept for backward compat.All three registration sites updated:
tools()descriptors,handleCalldispatch,server.gopackage doc.id/content_hashchoiceid= the brain-relative slash path — the de-facto handle today.brain_get(id)andbrain_get(path)resolve identically, and theidreturned by write/update round-trips throughbrain_get(covered by tests).content_hash= lowercase hex sha256 of the full file bytes on disk after the write.brain_getrecomputes from the file → identical hash.brain_updatehashes the prior file before rewrite and stamps it assupersedes.Step 0 — embedding re-index (the load-bearing finding)
No write handler re-embeds in-handler — not even
brain_write. The MCPServerholds only the read side (s.vectorVectorSearcher +s.embedderquery embedder); it has novectorstore.StorewithUpsert/Delete. Embeddings are refreshed exclusively out-of-band by:vectorstore.StartSync— background ticker (cmd/server/main.go:301, default 5 min). mtime-driven: re-embeds any file whose mtime is newer than its oldest chunk'supdated_at, deleting stale chunks first.POST /backfill-embeddings— manual full sync.So "re-sync on update" = ensure the out-of-band Sync re-embeds the changed file, exactly as the dispatch anticipated.
brain_updaterewrites the note viaos.WriteFile, advancing its mtime past the stored chunks'updated_at, so the next Sync pass re-embeds the new body and drops the stale chunk. I deliberately did not invent an in-handler embed call — that would fork a second mechanism diverging frombrain_write.This is satisfiable, not a blocker —
TestBrainUpdateReembedsNewContentdrives the realvectorstore.Syncafter an update and asserts the new body is re-embedded and the old chunk deleted (no stub of the re-index path).Design note for review: semantic (vector) read-after-write has a ≤5 min window — the supersede is visible to BM25/lexical immediately and to
brain_getsynchronously, but pgvector retrieval lags until the next Sync tick. If synchronous semantic refresh is ever needed, the path is to wire avectorstore.Storeinto the MCPServerand embed in-handler — a follow-up, out of scope for #45.Acceptance criteria
brain_updatesupersedes: body replaced,updated_at/supersedesstamped, prior fields preservedbrain_updateon non-existent target → clear error, no note createdbrain_updatereturns{id, path, content_hash}brain_writereturn contract extended to{id, path, content_hash}brain_get(id|path)returns the landed note (frontmatter + body)task checkpassesserver.go(all three sites)Notes
chore(context): re-sync derived adapters) —task check's context gate flagged pre-existing drift from the rootAGENT.mdrule-0 update; committing the regen is the documented remedy. Not a content change in this repo.Closes #45
🤖 Generated with Claude Code
Step 0 — embedding re-index trace (do-this-first finding)
Traced how a written note's embeddings get (re)built before writing any re-sync code, as instructed.
Finding: there is no in-handler embedding re-index anywhere — not in
brain_update, not inbrain_write, not inbrain_ingest. The MCPServeronly holds the read side:s.vector(search.VectorSearcher) ands.embedder(query-timesearch.Embedder). It has novectorstore.Store(theUpsert/Deleteinterface), so it cannot embed in-handler without new wiring.Embeddings are refreshed strictly out-of-band, two ways:
vectorstore.StartSync— background ticker, wired atingestion/cmd/server/main.go:301, default 5 min. It is mtime-driven (vectorstore/sync.go): for each file it compares the file mtime against the oldestupdated_atamong that file's chunk rows; if the file is newer it deletes all old chunks and re-embeds from scratch (handles shrink/orphan rows cleanly).POST /backfill-embeddings— synchronous full sync overwiki/+knowledge/.Consequence for the contract: "re-sync on update" means ensure the out-of-band Sync re-embeds the changed file — exactly the alternative the dispatch flagged.
brain_updaterewrites the note withos.WriteFile, so its mtime advances past the stored chunks'updated_at, and the next Sync pass re-embeds the new body + drops the stale chunk. This matchesbrain_writeprecisely. I did not add a divergent in-handler embed call.Not a blocker. The acceptance criterion "a query reflects the new content, not the old" is verified against the real mechanism in
TestBrainUpdateReembedsNewContent: write →Sync(embeds OLD) →brain_update→Sync(re-embeds NEW, deletes old chunk), asserted with a recording embedder. No stub of the re-index path.One design question for you (not blocking #45): semantic/vector retrieval has a ≤5 min staleness window after a supersede (BM25 +
brain_getare immediate). If the session-retro skill needs vector read-after-write within seconds, the fix is to wire avectorstore.Storeinto the MCPServerand embed the single changed file in-handler. I scoped that out as a follow-up — flagging per your "stop and raise it before building the rest" instruction. Everything here works against the existing architecture as-is.