# Journal — 2026-06-22 ## OKF (Google Open Knowledge Format) alignment — step 1: store frontmatter Google published OKF v0.1 (2026-06-12): an org-knowledge format that is near-identical to demarkus's own model — directory bundles of markdown, cross-link relationship graph, `index.md` hubs, path-minus-`.md` as concept identity. Spec: github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md ### The one real seam OKF carries metadata **in-body** as YAML frontmatter (required `type`; recommended `title`/`description`/`resource`/`tags` (a YAML list)/`timestamp`). demarkus carries metadata **out-of-band** and treats a body that opens with `---` as literal content. So syntactically identical (`---` frontmatter), but different data model: in demarkus the on-disk `---` block is a store-owned envelope (`version`/`previous-hash`/`archived` + publisher data), it is **stripped before serving**, and the catalog reads only the publisher keys. Naively publishing a raw OKF file double-wraps the frontmatter and the OKF `type`/`tags` never reach the catalog. ### Decision — split namespace in store frontmatter (`protocol/store/store.go`) Pre-1.0, so changed the storage format. Promote the recognized OKF field names to **bare** frontmatter; keep everything else namespaced: - Recognized OKF keys (`type`,`title`,`description`,`resource`,`tags`,`timestamp`) → written bare. `tags` serialized as a YAML flow list `[a, b]` to match spec. - Non-spec publisher keys (`importance`, custom) → keep the `meta.` prefix. - Store-operational (`version`,`previous-hash`,`archived`) → bare, unchanged. ### The non-obvious why The `meta.` prefix was not decoration — it was the **integrity boundary** that stopped a publisher from supplying `archived: true` / `version: 999` and forging store state (`extractMetadata` read only `meta.*`). Dropping it for the OKF fields means that boundary now has to be enforced by name: added a `reservedMetaKeys` denylist (`validateMeta` rejects them) and `extractMetadata` excludes them. Keeping arbitrary publisher keys under `meta.` preserves the boundary for the open-ended set — only a closed, known set of OKF names goes bare. Key property that kept the change contained: the **in-memory metadata map stays bare-keyed**, so catalog/handler/filter were untouched. Only `buildVersionFile` (write) and `extractMetadata` (read) changed. `tags` round-trips list↔csv, so the map contract (`"a,b"`) is unchanged. Back-compat: `extractMetadata` still reads old `meta.tags`/`meta.type` writes — live `soul.demarkus.io` data and existing immutable versions keep working. ### What this does NOT do Vocabulary alignment only. The frontmatter is still **stripped before serving** and the store's `versions/` layout is not an OKF bundle tree — so demarkus does not yet serve or ingest OKF bundles. That is the next layer: an import/export codec (and possibly server-native bundle serving). Because the stored field names are now the literal OKF names, that codec is 1:1 rather than a translation table. Open scoping question parked: import-only vs round-trip codec vs server-native content-negotiated bundle serving; and where it lives (`tools/demarkus-okf` CLI vs `client/okf` package). ## OKF — spec + ADR follow-up Documented the alignment rather than claiming compatibility: - `docs/SPEC.md` §9.4 rewritten to match the code (was already stale — never documented `archived` or the old `meta.` keys). Now specifies reserved operational fields, bare OKF field names, `tags` as a YAML flow list, the `meta.` prefix for non-spec keys, and that the store block is stripped before serving. §8.1 notes arbitrary publisher metadata + reserved-key rejection. §13 Future Extensions adds OKF interop as a planned codec, not a conformance claim. - `docs/adr/0002-okf-metadata-alignment.md` records the split-namespace decision and the integrity-boundary reasoning. Framing decision (Fritz agreed): do **not** assert "OKF compatible / superset" in normative spec text yet — it would overclaim. Accurate framing is two-layer: a single demarkus document's **content model** is OKF-compatible (the doc itself), but at the **system level** demarkus is a superset — it builds versioning + hash chain + QUIC + capability auth + LOOKUP around an OKF-compatible document. "The doc is OKF; we built more around it." Earn the compatibility badge with the codec + a conformance check against OKF sample bundles. ## Publisher metadata caps raised (10/512 → 50/1024) Bumped while sizing for OKF producer-defined fields. Key insight: `MaxMetaBytes` (sum of key+value lengths) is the **binding** limit, not key count — the 6 OKF fields + importance already use ~7 keys / ~350 bytes, so you hit the byte wall before the key wall. Doubling keys alone would have bought little. Final: `MaxMetaKeys` 10→50, `MaxMetaBytes` 512→1024, `maxStoreFrontmatter` 1024→2048 (must cover 1024 bytes meta + operational fields + per-line serialization overhead; worst case ~1.5 KB on disk). 50 keys is generous headroom but harmless — the 1024-byte total still bounds real on-disk size. Pre-1.0 is the cheap moment to right-size a wire limit. Constants in `protocol.go` + `store.go`; both store and handler validation read the shared constants. SPEC §9.4 updated. Boundary test now ties to `MaxMetaKeys+1` with short keys so it isolates the key-count limit and won't rot on future bumps. ## Gotcha: tags counted as csv but stored as a longer YAML list `MaxMetaBytes` (1024) is validated on the metadata map, where `tags` is a csv string (`"a,b"`), but on disk `tags` serializes as a YAML flow list (`[a, b]`) — `N+1` bytes longer for `N` tags. So the byte cap *undercounted* the real on-disk size, and a tag-heavy doc could pass validation then bloat the frontmatter. Adversarial worst case (~460 single-char tags + 49 tiny keys) reached ~2005 of the 2048 `maxStoreFrontmatter` budget — safe but only ~40 B margin, far thinner than the 1024 number suggests. Fix: extracted `store.SerializedMetaSize(key, value)` — counts the value at its on-disk length (tags → `formatTagsList`) — and used it in BOTH the store `validateMeta` and the handler size check (they duplicated the accounting; per guidelines, the tricky piece now has one source of truth). The 1024 cap is now an honest on-disk bound, so `maxStoreFrontmatter`'s margin is provable (worst case ~1542 < 2048) and a doc that passes meta validation always fits the frontmatter. Regression test: 400 one-char tags (csv 803 B passes, serialized ~1204 B rejected). `protocol.go` MaxMetaBytes comment updated to say values are counted as serialized. ## OKF codec — first slice: `demarkus okf validate` landed New package `client/internal/okf` (parse primitives import/export will reuse) + `demarkus okf validate` command (wired into the main.go subcommand switch). Primitives: `SplitFrontmatter` (CRLF + EOF-delimiter tolerant), `ParseFrontmatter` (flat string map; flow `[a,b]` and block `- a` lists → csv; tolerates comments; errors on malformed lines), `ConceptID`, `IsReserved`. Reuses `client/links` (goldmark) for link extraction — no new deps. `ValidateBundle` checks OKF v0.1 conformance: every non-reserved `.md` has parseable frontmatter with non-empty `type` (Error); non-root `index.md` with frontmatter (Error); broken/escaping internal `.md` links (Warn, per spec's tolerate-broken-links rule); `log.md` non-ISO date headings + non-okf_version root-index keys (Warn). Findings sorted (path, message) for stable output. Command: `--strict` (warns→failure), `--quiet`; exit 1 on errors. Deliberately hand-rolled the frontmatter parser (no YAML dep) to match the project's existing manual-frontmatter approach in the store. Limitation: handles scalars/flow/block lists, not nested maps or multi-line scalars — fine for OKF v0.1's flat field set; revisit if a sample bundle needs more. Next slices: `import` (bundle → world, enforcing the metadata caps with sanitize-on-overflow) then `export`. Tested against synthetic bundles; should run it against Google's GA4/StackOverflow/Bitcoin reference bundles once import exists. ## OKF codec — second slice: `demarkus okf import` landed `client/internal/okf/import.go` + `demarkus okf import [--dry-run] [--auth] [--insecure] `. Pure transform `BuildImport(root, prefix) []PublishItem` (no network, fully tested): per file — SplitFrontmatter, ParseFrontmatter, map OKF fields → demarkus metadata (recognized names are identity), strip frontmatter from body, rewrite bundle-absolute links under the prefix. The command publishes each item via `fetch.Client.Publish` with expectedVersion -1 (upsert; content-hash dedup means re-import of an unchanged bundle creates no new versions). The target URL's path IS the prefix (via ParseMarkURL) — no separate flag. Decisions / non-obvious bits: - **Metadata sanitation, never silent.** Invalid keys (e.g. `data_steward`) sanitized to `data-steward` with a warning; un-sanitizable or reserved keys (`version`/`archived`/`previous-hash`) dropped with a warning. Added `store.IsReservedMetaKey` so import reuses the store's reserved set rather than duplicating it. - **Cap enforcement reuses `store.SerializedMetaSize`** (honest tags-as-list accounting). Overflow drops lowest-priority keys first via a priority ladder (title>tags>type>importance>description>resource>timestamp>producer), each drop warned. Kept caps at 50/1024 per earlier decision; sanitize-on-overflow. - **Reserved files** (index.md/log.md) published verbatim; root index.md's okf_version frontmatter is stripped from the body (else it double-wraps) and kept as `okf-version` metadata. - **Link rewriting**: only bundle-absolute (`/x.md`) links get the prefix — relative links resolve unchanged since the tree is preserved. Limitation: empty-text links `[](/x.md)` aren't rewritten (ExtractWithPositions reports no bracket span); rare, noted. Deferred: live server round-trip. Server-side acceptance of bare OKF metadata + tags-list round-trip is already covered by handler/store unit tests, and the wire path by other CLI tests, so the marginal risk is low and the setup (TLS + TOML tokens + 2 procs) is high. Becomes natural once export lands: import → export → diff. Next slice: `export`. ## OKF codec — third slice: `demarkus okf export` + verified live round-trip `client/internal/okf/export.go` + `demarkus okf export [--auth] [--insecure] `. Completes the codec. Pure `BuildExport(docs, prefix) []BundleFile` (no network): per doc — strip the world prefix from path and bundle-absolute links, reattach OKF frontmatter from metadata (canonical field order: type, title, description, resource, tags, timestamp, then producer keys sorted). Synthesizes a `type` (default "Document") when absent and `timestamp` from the doc's modified time. `tags` re-serialized via the shared `store.FormatTagsList` (one source of truth with the on-disk form). Reserved files written body-only; root index.md re-emits `okf_version` from the `okf-version` metadata the importer stashed. Refactors / reuse: - Extracted `mapAbsLinks(body, fn)` from import's link rewriter; import prepends the prefix, export strips it (boundary-safe: only `/pfx` or `/pfx/...`). - `yamlScalar` quotes only when a bare scalar would reparse differently (`: `, trailing `:`, ` #`, surrounding space, empty, leading YAML indicator). URLs (no `: `) stay bare. - Command: `enumerateDocs` recurses LIST (the LIST verb always lists, never serves index.md — confirmed in handler; `versions/` already filtered by ListDir). `publisherMeta` strips server-owned response keys (status/version/ modified/etag/content-hash/current-version/entries). Bug found + fixed via the live test: import treated PUBLISH status `created` as failure — a new-doc publish returns `created`, only an update returns `ok`. Now accepts both. **Live round-trip verified** (self-signed server on :16309, minted read+publish token): import bundle → on-disk frontmatter exactly as designed (bare `tags: [sales, revenue]`, `title`, `type`; `meta.data-steward` for the sanitized producer key; absolute links rewritten under `/vendor`, relative preserved) → re-import upserts with content-hash dedup (no new versions) → export → `validate` reports 0 errors / 0 warnings → exported body byte-identical to original, links restored to bundle-relative, root `okf_version` round-tripped. Codec complete: validate + import + export, all three `demarkus okf` subcommands, shared logic in `client/internal/okf`, no protocol/server changes beyond the two small exported store helpers (IsReservedMetaKey, FormatTagsList). Next: point validate/round-trip at Google's real GA4/StackOverflow/Bitcoin sample bundles as fixtures; consider `log.md` generation from VERSIONS on export. ## OKF codec — timestamp format handling closed Closed the gap where incoming `timestamp` values were passed through unchecked. - **validate**: `validateConcept` now warns when a present `timestamp` doesn't parse as RFC 3339 (`isRFC3339`). Stays a Warn — OKF lists timestamp as recommended, not a hard conformance rule. Refactored validateConcept to accumulate findings (type error + timestamp warn) instead of returning early. - **export**: `normalizeTimestamp(declared, modified)` re-emits a present value in canonical RFC 3339 (accepts RFC 3339 or date-only `YYYY-MM-DD`, converts offsets to UTC); empty or unparseable → falls back to the doc's `modified` (already RFC 3339). So exported timestamps are always canonical. Required `type` was already covered: validate errors on missing/empty; export synthesizes `type: Document`; import warns but stays permissive. Tests added for both timestamp paths. All client tests + pre-commit green. ## OKF made a core write-time value: default `type` on publish Fritz wanted OKF conformance to be an opinionated, core demarkus value enforced on write. Pushed back on the obvious hard-reject gate: it would break every existing writer (soul.demarkus.io, knowledge broker worlds, journals, ADRs, docs site — all publish typeless markdown via mark_publish), and OKF's own first principle is "minimally opinionated." There was also no existing policy/gate machinery in the codebase to hook into. Chose opinionated-by-construction: on PUBLISH, when no `type` is declared, the server assigns `Document` (`protocol.OKFDefaultType`) — `applyOKFTypeDefault` in handlePublish, after extractPublisherMeta. Reserved files (index.md, log.md) exempt; explicit types preserved. Same constant backs export's synthesis (one source of truth). ADR 0003 records it; SPEC §6.4 + §14 updated. Now every served demarkus document carries an OKF type by construction — maximal per-doc conformance at write time. Still not a served bundle (frontmatter stripped, versions/ layout); full bundle conformance stays an export concern. Gotcha caught by the existing dedup test: the default is real metadata, so a legacy doc (written without type) republished identically now differs by the added type → one new version, then dedups. Fixed the test to seed v1 with type:Document; documented the one-time bump in SPEC + ADR. Deferred: a `strict` reject mode as opt-in config/per-world policy (never global default). ## OKF type default folded into APPEND too Extended `applyOKFTypeDefault` to `handleAppend` (after extractPublisherMeta), so every write path — PUBLISH and APPEND — guarantees a typed OKF concept. Noteworthy finding while doing it: **`store.Append` does not merge metadata** — it passes the request's metadata straight to `WriteVersion`, so an appended version's publisher metadata comes from the APPEND request alone, replacing the prior version's. An append with no metadata therefore drops the doc's tags/title/type. With the default applied, a typeless append at least lands `type: Document` instead of nothing. The broader "append replaces rather than carries forward metadata" behavior is pre-existing and orthogonal — flagged as a possible PR-comment item (should append inherit the prior version's metadata when the request omits it?). SPEC §6.4/§6.6 + ADR 0003 updated to cover APPEND; test added. All suites + pre-commit green; restaged on feat/okf-compatibility. ## Shipped — OKF compatibility merged (#195) Merged to main as squash commit 69c1473 `feat(okf): Open Knowledge Format compatibility (#195)`. Whole feature in one PR: store metadata alignment (bare OKF field names, tags-as-list, 50/1024 caps, reserved-key boundary), the `demarkus okf` validate/import/export codec, server default `type` on publish + append, SPEC updates, ADR 0002/0003. CodeRabbit review addressed (7 fixed: export traversal guard, import symlink rejection, LIST decode/cycle hardening, reserved-key read guard, cap re-validation→bad-request, link query strip, test assertions; 1 skipped: SPEC markdownlint, not CI-gated, deferred to roadmap Docs Hygiene backlog). Local feature branch deleted. ## `/soul-join` — managed remote souls + catalog + project binding (built, branch `feat/soul-join`) Problem Fritz raised: three demarkus surfaces configurable at once (hand-wired `.mcp.json` "pure MCP", local memory plugin, knowledge plugin) all expose identical `mark_*` tools, so the agent gets confused which to write to. Live proof: this project uses `demarkus-soul` → soul.demarkus.io via hand-wired `.mcp.json`, while the memory plugin's local backend is down — two "soul" surfaces, one tool name. I conflated them in conversation, which *is* the bug. Decisions (Fritz): build `/soul-join` into demarkus-memory so there's no manual MCP for souls; token storage = wrapper + 0600 file (not inline); maintain a **catalog** of souls + a **per-project binding** for routing; detect existing hand-wired entries and offer to adopt. Plan at `/plans/soul-join.md`. Shipped (plugin v0.9.0 → v0.10.0): - `lib.sh`: catalog (`~/.demarkus/souls`, tab rows `slug\thost\tinsecure\ttoken-file`) + binding (`~/.demarkus/project-souls`, `dir\tslug`) helpers (register/fields/list/is-registered/bind/binding, all idempotent upserts). Extended `publish_gate_scope` so the tag-gate fires on joined remote souls too. - `scripts/soul-join.sh`: normalize host (mark:// scheme, reject https→knowledge), derive+sanitize slug from first DNS label, reject reserved `demarkus-memory`, token→0600 file, register, optional `--bind`. Best-effort reachability only (QUIC has no HTTP metadata to probe). Test hatch `DEMARKUS_SOUL_JOIN_SKIP_BINARIES`. - `scripts/soul-remote-wrapper.sh`: self-contained (sources nothing), installed to the STABLE path `~/.demarkus/bin/` so a plugin upgrade can't strand the registered MCP server. Reads the catalog, injects `DEMARKUS_AUTH` from the 0600 file, execs `demarkus-mcp -host … [-insecure]`. - `scripts/detect-manual-souls.sh`: parses `claude mcp list` for direct demarkus-mcp+mark:// entries (excludes wrapper-managed ones), emits name/host/insecure/has-token — token VALUE never echoed (transcript safety). - `commands/soul-join.md`, session-guidance "which soul to write to" section, `tests/soul-join_test.sh` (16 tests). Bug caught in smoke test: wrapper didn't `shift` the slug arg, so it leaked into demarkus-mcp's argv as a stray positional. Fixed. Verified: 16 new + all existing plugin tests pass; pre-commit green; shellcheck clean; wrapper smoke (auth+insecure / no-auth+secure both correct); detect-manual-souls correctly finds `demarkus-soul` + `demarkus-hub` and skips the managed local soul. Open / deferred (phase 2): destination GATE (PreToolUse on mark_publish enforcing the project→soul binding, not just guiding); full auto-migration of a hand-wired entry (currently guided re-join + `claude mcp remove`); reachability hardening; wrapper auto-reinstall on plugin upgrade. NOT bumped: lib.sh SERVER/CLIENT/TOOLS binary pins — convention says bump per plugin change, but this is plugin-only with no binary release; left for Fritz to decide at release. Fritz commits. ## `/soul-join` follow-ups: binary-upgrade server restart + destination gate Two more on `feat/soul-join`. ### Bug: binary swap didn't restart the local server (all modes) `ensure_binaries` upgrades the on-disk binary but only session-start's managed arm restarted the server (via the `.server-version` stamp in `ensure_managed_server`). Two gaps: (1) `/soul-join` calls `ensure_binaries` for `demarkus-mcp` and could swap the SERVER binary mid-session without restarting — a bug I introduced; (2) reuse mode is never restarted by design, so a binary bump leaves it on the stale binary (this is exactly the down-server-at-`content` symptom). Fix: `ensure_binaries` sets a `DEMARKUS_BINARIES_REPLACED` global only when it actually swaps a binary. New `restart_local_server_on_upgrade` (lib.sh) reads it, loads the config in a subshell (no var clobber), and restarts the configured server with its OWN recorded root+port. Relaxes the old "never restart a reuse server" policy per Fritz's ask ("find the local server config and restart with the same config"): for an externally-started/reuse server (no `.pid` of ours) it stops the process found at the root first so the respawn binds the freed UDP port, then `ensure_managed_server`. Wired after `ensure_binaries` in both session-start and soul-join. Non-fatal (warns, never aborts the caller). 5 tests stubbing `ensure_managed_server`/`pid_of_server_at_root` (incl. a real stop-external-then-respawn using a `sleep` stand-in). ### Phase 2 shipped: destination gate (binding enforced, not just guided) New PreToolUse hook `hooks/dest-gate.sh` on `mark_publish` AND `mark_append`. When a project has a binding (`project_soul_binding`), a write to a *different* soul is a misroute → denied at write time. Scope is narrow + fails open: acts only when the tool targets a soul this plugin routes (`soul_target_id` → `demarkus-memory` for local, slug for a joined remote, empty for knowledge/ unrelated → deferred) AND a binding exists. No binding → defer, so the vast majority of projects see zero change. Strictness `block` (default — agent self-corrects to the bound soul, no human interrupt) | `ask` | `warn`, via `DEMARKUS_MEMORY_DEST_STRICTNESS` / `plugin-memory.dest-strictness`. Separate axis from the tag-gate strictness. 11 tests. Design note: default `block` (not `ask`) is deliberate — an explicit `/soul-join` binding means writes belong to that soul, and for an agent a deny + reason triggers a silent retry to the right server, which is *less* disruptive than asking the human every time. Relaxable per-repo/env. Session-guidance updated: "which soul to write to (catalog + binding)" now states the binding is enforced. Full suite green (112 tests across 12 files), shellcheck clean, pre-commit green. Still plugin-only; binary pins untouched (Fritz's call at release). Phase-2 remainder: full auto-migration of a hand-wired entry; append-metadata carry-forward is an orthogonal pre-existing item. ## `/soul-join` — reuse-restart softened + #197 pin-automation reconciled Two corrections to the above. ### Reuse restart: only start a DOWN server, never kill a healthy one Reversed the earlier "stop the external server then respawn" behavior (Fritz: "only start a down reuse server, never kill a healthy one"). `restart_local_server_on_upgrade` now branches by ownership: OUR managed server (our `.pid`) → `ensure_managed_server` restarts it; a server we don't own (reuse/external) → if healthy, leave it untouched and warn it's still on the old binary (user restarts on their terms); only if DOWN do we start it on the new binary with the recorded config. No kill path at all now — simpler and safer. Test renamed to `reuse_healthy_external_left_alone` (asserts not-killed AND not-respawned); managed test now writes a `.pid` to exercise the owned branch. ### Binary-pins note was moot — #197 already automated it My "binary pins untouched, Fritz's call at release" caveat is obsolete: #197 (`chore(plugin-memory): bump to OKF release + automate future pin bumps`, already in main and this branch's base) added `.github/scripts/bump-plugin-pins.sh` + `.github/workflows/plugin-pin-bump.yml` — a daily/on-release workflow that opens a PR bumping SERVER/CLIENT/TOOLS pins to the latest releases and the plugin patch version to match. So NOT hand-bumping pins was correct, and it's now mechanized. The old conventions.md rule "bump plugin pin versions on every update" is superseded by that automation. My manual 0.9.0→0.10.0 minor bump (for the feature) doesn't conflict — the workflow no-ops when pins already match latest (they do: 0.18.0/0.13.0/0.2.0). Branch verified current with main (nothing ahead), full suite 112 green. ## `/soul-default` — standalone default-binding command (built) Fritz wanted a way to set this project's default write target *without* re-running `/soul-join`. Until now the binding (`~/.demarkus/project-souls`) was only ever written as a side-effect of `/soul-join --bind`, so re-pointing an already-joined repo meant a full re-join. New `/soul-default`: list the catalog, pick one, save it. Naming his (`soul-default`, clearer than "bind"). Design question he posed and we settled: *"is the plugin smart enough to use another server, or does that need a tool?"* — **neither a router nor a dispatch tool.** Routing is already expressed by which `mcp____mark_*` the model calls; each joined soul is its own MCP server. The plugin's job is only the **default binding** + the **dest-gate** guardrail. A `mark_publish_to(target,…)` tool would force the local soul's server to *proxy* to remotes — duplicating MCP's per-server routing and breaking "each soul = its own server." So: the **catalog is the discovery surface** (agent reads `~/.demarkus/souls` to know what exists), the **binding is just the default**, reads/one-off writes to any other joined soul go direct to its tools. Confirmed reads are already ungated (dest-gate fires only on publish/append). Built (follows the soul-join script+command+test pattern): - `lib.sh`: `local_soul_present`, `soul_catalog` (unions local managed soul + remote rows → `id\ttier\thost\tinsecure` — fulfills the `soul_catalog()` promise the SOULS_REGISTRY header comment already made), `is_catalog_soul` (validates a slug is actually joined before binding, so a binding can't point at an unjoined soul). - `scripts/soul-default.sh`: `--list --bind DIR` (catalog, current default marked `*`) and `--set SLUG --bind DIR` (validate → `bind_project_soul`). Joins nothing, touches no MCP config/token, reuses the awk upsert so only this project's row changes. - `commands/soul-default.md`: list → AskUserQuestion → set. - `tests/soul-default_test.sh`: 10 tests. Verified: soul-default 10/10, soul-join 16/16 (lib changes safe), dest-gate 11/11; shellcheck clean (`-x`). ### Gotcha — the "one-off override" premise is false in current code The original /soul-default management prompt (and earlier conversation framing) claimed *"the destination gate honors explicit per-call targeting, so a one-off write to another soul needs no file edit."* **Not true today.** dest-gate scopes to *any* joined soul (`is_registered_remote_soul`) and compares `soul_target_id` to the binding — so a deliberate `mcp__hub__mark_*` while bound to `soul` is **denied** (strictness `block`). There is no override path in the code. Fritz scoped this build to list+select+save only ("that is it"), so the override was deliberately NOT built. Two ways to make the claim real if revisited later: (a) a gate-honored one-shot signal (e.g. a `~/.demarkus/dest-override` sentinel the gate consumes once), or (b) default the gate to `warn`. Parked. The stale "honors per-call targeting" wording in the management prompt should be corrected when the override is actually built. NOT committed (Fritz commits). Binary pins untouched (plugin-only; #197 automation no-ops since pins already match latest). ## `/soul-default` — shipped (PR #202 merged to main) Follow-up to the build entry above. Bumped plugin 0.10.1 → 0.11.0 (plugin.json + marketplace.json), branch `feat/soul-default`, PR #202. CodeRabbit (ASSERTIVE) flagged 3, all valid, all taken: - **lib.sh soul_catalog masked read errors as empty** (Major). Was `awk … 2>/dev/null || true` — a present-but-unreadable SOULS_REGISTRY would print nothing and the CLI `--list` would report EMPTY ("run /soul-join") when the real fault is permissions. Fix: `[[ -e ]] || return 0` (absent = no remotes) then `[[ -r ]] || return 1` (unreadable = propagate), awk unguarded. Note this makes soul_catalog stricter than its sibling `list_remote_souls` (still fails-open) — accepted, since soul_catalog feeds the user-facing list. - **soul-default.sh --set accepted a flag as the slug** (Minor). `--set --bind X` took `--bind` as SLUG then failed later with a misleading "unexpected argument". Fix: reject `-*` values (slugs are sanitized [a-z0-9-], never lead with `-`) → direct "requires a soul slug". - **No CLI test for the local branch** (nit). Added `test_set_local_when_configured` (end-to-end `--set demarkus-memory` via seed_local) + `test_set_rejects_flaglike_slug`. 12 tests now. Title check also flagged: corrected PR title scope `tools` → `plugin-memory` (repo convention) + grammar via `gh api -X PATCH` (plain `gh pr edit` hit the Projects-classic GraphQL deprecation bug). Skipped CodeRabbit's "docstring coverage 50%" pre-merge warning — bot heuristic miscount; every function carries a comment block per repo convention. Merged squash `ce700d8`. Verified post-merge: all three fixes present on main, cache 0.11.0 byte-matches main, `/reload-plugins` picked it up, `/soul-default` live (this repo bound to `soul`). Fritz merged (I don't commit/push). ## Release cleanup — pruned GitHub releases 39 → 8, kept all tags After the OKF set shipped (protocol 0.8.0 / server 0.18.0 / client 0.13.0 / tools 0.2.0), cleaned up the accumulated patch-release noise on GitHub. Retention policy chosen: **latest minor + one prior anchor per module**. Kept 8: client v0.13.0+v0.12.43, server v0.18.0+v0.17.19, protocol v0.8.0+v0.7.13, tools v0.2.0+v0.1.38. Deleted the other 31 (client 0.12.34–42, server 0.17.10–18, tools 0.1.25–37) — all CI auto-bump patch releases. ### Why it's safe (checked before deleting) - `install.sh` resolves the **latest** release per module (`fetch_latest_version` greps the releases API for the newest `/v*` tag) — old releases serve no install purpose. - The plugin pins the current OKF set (`lib.sh` SERVER 0.18.0 / CLIENT 0.13.0 / TOOLS 0.2.0) and `bump-plugin-pins.sh` auto-tracks latest — never references old. - Internal builds use `replace … => ../protocol|client` with `v0.0.0` placeholders (go.mod), so they do NOT resolve published tags — deleting tags wouldn't even break the monorepo's own builds or the next release's changelog base (kept the immediately-prior anchor per module). ### Key decision: delete the RELEASE, keep the TAG `gh release delete --yes` **without** `--cleanup-tag`. Rationale — tags cost a few bytes and deleting them adds real risk for zero space saving: - **Go proxy/checksum poisoning** (the nasty one): these are public Go modules; any fetched version is cached immutably by proxy.golang.org + sum.golang.org. Deleting the git tag doesn't purge them, AND if a deleted tag name is ever reused on a different commit → consumers get `checksum mismatch / SECURITY ERROR`. Never-reuse is the only safe rule. - External `go get module@vX` breaks (source side) for versions not already proxied. - Tag deletion is non-durable anyway — a later `git push --tags` resurrects them. - Loses `git checkout ` reproducibility + provenance link for Docker images. So: cleaner Releases page, every version still rebuildable from its tag via GoReleaser. Verified: 8 releases remain; spot-checked deleted-release tags (client/v0.12.34, server/v0.17.10, tools/v0.1.25) still present on origin; 192 remote tags untouched. Gotcha noted: old prebuilt binary ASSETS are gone (no download-a-prebuilt rollback to a deleted version), but rebuild-from-tag works.