Completed Plans
Archive of implementation plans that have been executed and shipped.
Content Addressing — COMPLETED ✓
Completed: March 2026 (Phase 3)
Hash-based fetch, in-memory index, mirror foundation. All five implementation steps completed:
content-hashadded to FETCH responses — SHA-256 of stripped body in metadata- Hash index in Store —
BuildHashIndex(),LookupHash(),UpdateHashIndex(),RemoveHashEntry() - Index updates on writes —
Write(),SetArchived()keep index synchronized - Hash-based FETCH —
isHashPath()validates/sha256-<64hex>,handleFetchByHash()retrieves by hash - Startup initialization —
BuildHashIndex()called on server startup
Key details: Content-hash is separate from etag (stripped body vs full doc). Current versions only indexed. Read auth checked after hash resolves to real path. In-memory index rebuilt on startup.
Foundation for: Federation, content-addressed mirroring, distributed caching
Federation — COMPLETED ✓
Completed: March 2026 (Phase 3)
Agent-driven hash discovery via MCP tools. Zero server changes, zero new verbs.
Shipped:
mark_indexMCP tool — crawls source server, collects hashes, publishes index to hubmark_resolveMCP tool — resolves content by hash using hub indexclient/internal/indexpackage — Parse, Build, Merge for markdown hash index documents- Manifest check enforced by tool,
forceoverride,dry_runmode, 1000 doc cap
Persistent Graph — COMPLETED ✓
Completed: March 2026 (Phase 4)
Disk-backed graph store, incremental crawl, backlinks.
Shipped:
client/internal/graphstorepackage — nodes, edges, etags, timestamps, atomic writes, schema versioningCrawlAndPersist— unified crawl + merge + save, nil-safe, shared across CLI/TUI/MCPmark_backlinksMCP tool — reverse link lookup- Graph export —
Store.Export()renders as publishable markdown,ParseExport()parses back - TUI graph view — Links (BFS), Backlinks, Topology sub-views
- Graph seeding — TUI loads instantly from stored graph while crawl runs in background
Read Auth (Server-Side) — COMPLETED ✓
Completed: 2026-03-14 (Phase 5)
Per-path read token enforcement on the server. Fully backwards compatible — no read tokens = everything public.
Shipped:
RequiresReadAuth(path)onTokenStore— pre-computedreadPathsat load timeauthorizeReadhandler helper — checks token store, exempts/.well-known/agent-manifest.md- Integrated into FETCH, LIST, VERSIONS handlers
- Content-addressed fetch respects read auth (hash resolves to path first, then checks auth)
- Versioned path auth —
/doc.md/v2checks auth on base path/doc.md - Directory path normalization —
/privateand/private/both match/private/**patterns
Remaining (Phase 5): Client-side read auth — fetch.Client read methods (Fetch, List, Versions) need a token parameter, then CLI, TUI, and MCP need to pass it through. The server enforces correctly; the clients just can't send a token on reads yet.
Conflict-Aware Merge in mark_publish — COMPLETED ✓
Completed: 2026-05-05 (PR #101 in client/v0.12.25 — diff3 + merge-candidate response; v0.12.26 — default flipped from "fail" to "merge")
Tool-level diff3 merge that reduces content loss under concurrent writes. The MCP tool produces a structurally-merged candidate body when mark_publish hits a version conflict; the agent semantically verifies the candidate and republishes. No wire-protocol changes; merge logic lives entirely in the Go client.
Shipped:
client/internal/merge/package —Diff3(base, ours, theirs) Result(in-package implementation, ~250 lines, line-based LCS + hunk walk, no external dependency) +Candidate(client, path, ours, expectedVersion, meta)orchestrating publish-or-merge in one shot.on_conflictparameter onmark_publish:"merge"(default since v0.12.26) returns a merge candidate on conflict;"fail"opts out to strict optimistic-concurrency semantics.- Git-style conflict markers in body (
<<<<<<</=======/>>>>>>>) — agents handle natively from training; format lives in client Go code and can be swapped without breaking the wire protocol. - LCS dp table capped at 2M cells (~16 MB) — pathological inputs (1 MiB body of 1-byte lines) fall through to a single-hunk merge rather than allocating gigabytes.
Candidaterejects responses wherelatest.Version <= 0to prevent silent "create-only" semantics from re-publishing into nothing.
Key decisions:
- Tool never auto-publishes a diff3 result. Always returns the candidate to the agent for semantic verification. Line-disjoint changes are not semantically-disjoint (duplicate bullets, contradictions, list reorder collisions can pass diff3 but corrupt the document).
- No internal retry loop in the tool. Each
mark_publishcall is one-shot; iteration lives in the agent's natural fetch-modify-publish loop. mark_appenddeliberately out of scope. Append-as-stream is the right primitive there; auto-resolve handles its tiny race window because there is nothing to merge.- Default flipped from
"fail"to"merge"in v0.12.26 because the"fail"-default left naive callers exposed to silent content loss — the exact failure mode the feature exists to prevent. The shape change (merge-candidatevsconflict) is loud, not silent.
Wire-level impact: zero. on_conflict is an MCP tool parameter handled in the Go client.
Claude Code Plugin — COMPLETED ✓
Completed: 2026-04-23 plan landed; demarkus-memory plugin v0.1.1 shipped on the marketplace; SessionStart hook + lazy-spawned server + auto-generated token in active use across every demarkus development session since.
One-click marketplace install gives Claude Code users a local, versioned memory layer backed by a spawned demarkus-server. Zero core code changes — the plugin is built entirely on existing primitives (DEMARKUS_AUTH env var, demarkus-token generate, ALPN-tagged /health, tokens.Resolve).
Shipped:
plugins/claude-code/plugin tree:.claude-plugin/plugin.json,hooks/postinstall.sh+hooks/session-start.sh,.mcp.json, slash commands (/soul,/soul-journal,/soul-status,/soul-init,/soul-context,/soul-memory),skills/memory/SKILL.mdtriggering on remember/save/recall intents,seed/index.mdtemplate..claude-plugin/marketplace.jsonat repo root for/plugin marketplace add latebit-io/demarkus.postinstall.sh— detects platform viauname -sm(darwin/arm64, darwin/amd64, linux/amd64, linux/arm64), downloadsdemarkus-server+demarkus-mcp+demarkus-tokenfrom GitHub releases, verifies SHA256 checksums against bundled file, installs to${CLAUDE_PLUGIN_ROOT}/bin/.session-start.sh— probes existing demarkus server on default port 6310 via ALPN-tagged/health(a non-demarkus process can't pass ALPN negotiation, so port-reuse is safe), spawns server detached when absent, generates a/*-scoped publish+archive token on first run, writes raw token to~/.demarkus/soul/.tokenmode 600, exportsDEMARKUS_AUTHbefore MCP client launches, copies seedindex.mdwhen content root is empty.- Plugin config at
~/.demarkus/plugin-memory.confrecords the chosenMODE(default | isolated | reuse),SOUL_DIR,PORT.
Key decisions:
- Lazy spawn, no service manager (no launchd/systemd in v1). Server persists across sessions; next session reuses it via the ALPN-tagged probe.
- No core code changes. Every primitive (
DEMARKUS_AUTH,/health,tokens.Resolve, thedemarkus-token generateCLI redirection pattern) already existed. The plugin is pure plumbing. - Content root at
~/.demarkus/soul/by default. All versioning, graph data, tokens, logs live under it. - Token mode
600on~/.demarkus/soul/.token.
In active use: the demarkus development workflow itself uses the plugin as the agent's memory layer — every session this conversation runs against the lazy-spawned server, every journal entry is a mark_append via the plugin's MCP wiring, the universe-deployment plan is fetched/republished through it.
Phase 6 — Universe Deployment (mostly complete)
Phase 6 (/plans/universe-deployment.md) is ~90% complete as of 2026-05-13: Helm charts for server / broker / agent, OIDC token broker binary + chart, kind-tested upgrade-wipe regression, per-service runtime images, OCI Helm chart publish via release pipeline, structured-slog observability with operator-facing schema doc. Remaining: §6.4 (topology examples) and §6.6 (ops-runbook docs), both under reframing per the "ops polish vs knowledge universe" cost/value pushback. Active plan stays at /plans/universe-deployment.md until §6.4 + §6.6 are resolved or formally deferred.
Broker MCP Gateway — COMPLETED ✓
Completed: 2026-05-23 (Slice 8 / PR #152 merged; full plan v7 closed)
Enterprise-facing MCP gateway on the demarkus-broker. Single /mcp endpoint exposing the 13-tool demarkus surface to plugin-style agents over JSON-RPC over Streamable HTTP, authenticated by the company SSO (id_token bearer through the PR4 compositeVerifier), with world access-tokens cached broker-side per session and never persisted. Solves the "20 dev teams × N worlds plugin-config grind" + corporate-network UDP-blocking problems in one. Plan archive: /plans/broker-https-gateway.md (v7 changelog at the top traces every load-bearing pivot from v1 REST → v7 complete).
Shape:
- HTTPS at the org boundary (
/mcpover Streamable HTTP per current MCP spec) terminates at the broker; corporate proxies, Ingress, DPI appliances pass it through unchanged. QUIC stays inside the cluster, where firewalls don't see it. - Identity is canonical verified email (trim + lowercase,
email_verified=truerequired) — same key the broker already uses for/me/install,/tokens,AllowConfig, audit logs. ONE identity dimension across every surface. - World access-tokens minted lazily per (canonical-email, world) via
Issuer.MintFiltered, cached in-memory with LRU + idle eviction, never persisted. Broker restart drops the cache; next tool call re-mints. Singleflight on concurrent first-call bursts. Retry-on-401-after-mint with exponential backoff absorbs the kubelet→world-Secret propagation lag. - Tool URLs carry the worldName as host:
mark://{worldName}/{path}. Broker resolves to cluster-internal Service DNS (<name>.<namespace>.svc.cluster.local:6309default, overridable viaworlds[].internalAddress). - Byte-for-byte markdown proxy.
mark_fetchthrough the broker returns the same body, version, etag, content-hash, and metadata keys asmark_fetchagainst the same world via stdio/direct-QUIC. Pinned by aformatToolResult/formatResultbyte-equal reference test that catches drift before shipping.
Shipped (in order):
- Pre-Flight 0 (#142, 2026-05-20): hoisted
client/internal/fetch+client/internal/mergeto public sotools/demarkus-brokercould import them across module boundaries. 5 files relocated, 9 consumers re-imported. - Pre-Flight 1 (2026-05-20):
mark3labs/mcp-go v0.44+shipsNewStreamableHTTPServeras a productionhttp.Handler; Stream Resumability is the only missing feature and it's already out-of-scope. - Slice 1 (#143, 2026-05-20,
b90cda6): foundation — listener + JSON-RPC dispatcher +initialize+tools/list(13 tool definitions, placeholder handlers) + OAuth metadata (/.well-known/oauth-protected-resourceRFC 9728 +/.well-known/oauth-authorization-serverRFC 8414, the latter aliasing the OIDC Discovery handler) +gatewayAuthmiddleware with RFC 6750+9728WWW-Authenticatechallenge. - Slice 2 (#146, 2026-05-21,
7c529d4): read toolsmark_fetch/mark_list/mark_versions+ email-keyedsessionCache+worldPool+ singleflight on (email, world) mints + retry-on-401 with exponential backoff (OQ#9 resolution). Proxy-fidelity test pins byte-for-byte parity with local demarkus-mcp. - Slice 3 (#147, 2026-05-21,
5387701): write toolsmark_publish/mark_append/mark_archive.dispatchWithAuthrefactor (closure-based, shared retry loop across read + write). Conflict + not-permitted forward verbatim (not as tool errors). - Slice 4a (#148, 2026-05-22,
115a09b): federation readsmark_discover+mark_resolve. Cross-org candidate skip semantics. Hoistedclient/internal/index→client/index. - Slice 4b+5 (#149, 2026-05-22,
fa9f86d): graph-store federation toolsmark_backlinks+mark_graph+mark_index+mark_graph_export+mark_graph_publishbacked by an ephemeral in-memory graph store (pod-scoped lifetime, re-crawl after restart). Hoistedclient/graphstore+client/graph+client/links. Brought all 13 tools to real handlers. Bucket-store-backed persistence parked for the post-broker design window (/thoughts.md§ "On Bucket Stores"). - Slice 6 (#150, 2026-05-22,
80a4008): conflict-aware merge inmark_publish.on_conflict="merge"candidate flow + default flipped from"fail"to"merge"to match local demarkus-mcp.brokerMergeAdaptercaptures ctx in the struct (only viable shape againstmerge.Client's ctx-free interface). - Slice 7 (#151, 2026-05-22,
277f83f): chart wiring (server.mcp.* block, worlds[].internalAddress, deployment second containerPort + optional TLS volume, servicemcpport, networkpolicy ingress port, ingress.mcp.host separate-hostname topology, parallel cert-manager Certificate). New chart README "MCP gateway" section + newtools/demarkus-broker/MCP-API.md13-tool spec. Kind harness--with-mcp-smokestage (builds broker locally, sideloads, installs LOCAL chart, runs RFC 9728 + 8414 metadata + 401-challenge checks). 99 helm-unittest cases green. - Slice 8 (#152, 2026-05-23,
bc0d5cc):/knowledge-joinplugin slash command + URL-validation/slug-derivation script + first shell-test suite underplugins/claude-code/tests/(11 cases, python3-mocked broker). Plugin v0.1.2 → v0.2.0. CLIENT pin 0.12.33 → 0.12.36; TOOLS pin 0.1.10 → 0.1.16 (drift catch-up). CI workflowpermissions:block fordorny/paths-filter@v3shipped same PR.
Key decisions worth remembering:
- Single identity dimension across the broker (canonical verified email). Pinned in v5 after the alternative (subject-hash-keyed sessions) would have given the broker two identity dimensions to keep in sync. Same canonical email across MCP,
/me/install,/tokens,AllowConfig, audit logs. - Gateway is always on, not a feature flag. Pinned in v4 after Slice 1 —
MCPConfig.Addrdefaults to:8081so pre-gateway YAMLs upgrade silently. The "OAuth-only deployment" shape was never a real product. - Byte-for-byte markdown proxy. Pinned as a Non-Negotiable in v4. The broker is a wire-shape adapter (HTTP/JSON-RPC ↔ QUIC/demarkus), not a content transformation layer. Federation, content-addressing, and downstream hash-equality all depend on this.
- Separate hostname for the MCP gateway (
ingress.mcp.hostparallel toingress.host). Avoids.well-known/*collisions between OIDC discovery (management API) and OAuth resource metadata (MCP). Independent cert rotation too. - Retry-on-401-after-mint (OQ#9 resolution, Slice 2). The kubelet→world-Secret propagation lag means a freshly-minted token can briefly 401 at the world. Broker-side bounded retry with exponential backoff (6 attempts, 250ms → 8s) absorbs the race; the alternatives (sync-wait on world health endpoint, or prewarm during MCP
initialize) were rejected — the former would have violated the "no demarkus-server changes" Non-Negotiable, the latter would front-load SIGHUPs for worlds the user may never touch. - Ephemeral graph store, not persistent. Slice 4b decision — broker is a wire-shape adapter, persistent state belongs elsewhere. Re-crawl after broker restart is the documented operator expectation. Bucket-store-backed persistence parked for post-broker (
/thoughts.md§ "On Bucket Stores"). - No in-line PEM mode for the chart's MCP TLS —
existingSecretRefonly (Slice 7). Cleartext private keys in helm release history are never acceptable, even for dev. - Slug heuristic = first DNS label of the broker hostname (Slice 8), not "strip-broker-and-com → org". Matches typical enterprise broker URL shapes (
mcp.broker.acme.com→mcp). For IP literals the heuristic degrades to a number — the slash command tells the user to rename viaclaude mcpif it's bad.
Worth pinning generally:
- Sprig's
intis a best-effort cast (cast.ToInt, notcast.ToIntE) —{{ "abc" | int }}returns 0 with no error. Any chart helper piping a user-controlled string throughintMUST pre-validate (regexMatch "^[0-9]+$") and range-check before the cast. Otherwise typos rendercontainerPort: 0and crash the pod with no breadcrumb. (Caught by CodeRabbit on Slice 7.) - String-compare on
host:portis wrong for collision detection.:8080,0.0.0.0:8080,127.0.0.1:8080,[::]:8080all bind the same port but compare as different strings. Extract the port number and compare numerically. (Caught by CodeRabbit on Slice 7.) - Backgrounded processes inside
$(...)command substitution inherit stdout fd —$()waits for stdout to close, so the subshell never completes until you kill the backgrounded process explicitly AND wait for its stdout to drain. Always redirect background process stdout to/dev/nullinside command substitution. (Hit in Slice 8 test runner with a python mock HTTP server.)
Plan archive: /plans/broker-https-gateway.md stays in place as the architectural reference and decision trail. The v1-v7 changelog at the top traces every load-bearing pivot.
Universe-onboarding rewire: the original PR6/PR7/PR8 of that plan were absorbed here — PR6 (tools/demarkus-join binary) canceled in favor of Slice 8's slash command; PR7/PR8 (docs + plugin slash commands) folded into Slices 7-8.
RFC 7591 DCR Follow-on (Broker MCP Gateway) — COMPLETED ✓
Completed: 2026-05-26 (PR #153, 14728a7)
Small but load-bearing post-merge follow-on to the Broker MCP Gateway. The kind/k8s deploy testing surfaced that Claude Code's native MCP authorization client refuses to proceed against an authorization server whose discovery doc omits registration_endpoint — the MCP authorization spec mandates RFC 7591 Dynamic Client Registration. /knowledge-join device flow was fine (static client_id=demarkus-cli), but the spec-driven OAuth dance was blocked.
Shipped:
tools/demarkus-broker/internal/broker/register.go— RFC 7591/registerhandler. Rubber-stamp by design (no persistence): mints a 128-bit random base64urlclient_id, echoes request metadata, force-pinstoken_endpoint_auth_method: "none"andgrant_types: ["urn:ietf:params:oauth:grant-type:device_code"], returns 201 withCache-Control: no-store.discovery.go—applyDiscoveryOverridesnow rewritesregistration_endpointto<brokerURL>/register. Field appears on both/.well-known/openid-configurationand/.well-known/oauth-authorization-server(sharedDiscovery.Handler()).server.go— routed under the sameipRateLimitmiddleware as the device-flow POSTs.- 9 subtests covering shape, echo, broker-field override, distinct IDs, invalid JSON → 400, oversized → 413, GET → 405,
Cache-Controlheader, forcedgrant_types.
Key decisions worth remembering:
- No client database. Broker has zero
client_idenforcement on the device-flow path (device.go:108accepts any non-empty string). Real authz lives at id_token verification + per-worldAllowlist. Persistence would be vestigial state until per-client policy (audit, redirect URI checks, per-client rate buckets) becomes a real requirement. Theregister.godoc comment names the exact moment to flip rubber-stamp into a real handler. - Force-pin grant_types, not reject. RFC 7591 §3.2.2 says the AS MUST respond
invalid_client_metadatafor disallowed grants. We normalize because the MCP authorization spec leansauthorization_code-first and the broker can't do auth-code anyway (noauthorization_endpoint). Force-pinning tells the client what they got rather than failing with "incompatible" at registration time. - Open registration, no
initial_access_tokengate. IP rate limiter is the only protection. Acceptable for the demarkus deployment posture (broker behind ingress); if a customer broker grows abuse exposure, gated mode keyed off the/me/installbundle is the natural escalation — same shape, no new infra.
CodeRabbit nits caught + addressed pre-merge:
- Missing
Cache-Control: no-storeon the 201 — added. grant_typesechoed caller input unrestricted — force-pinned.- Empty-body comment overclaimed RFC 7591 §3.1 — tightened.
Verification: broker test suite green (4.677s), pre-commit.sh clean across protocol/server/client/tools, end-to-end Claude Code → cluster broker auth dance confirmed in cluster.