Skip to content

engram Extraction Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Re-home the memory MCP server + its deployment chart + CI from fzymgc-house/selfhosted-cluster into a new standalone seanb4t/engram repo, then cut the running cluster over to the new OCI Helm chart without losing the Qdrant PVC data.

Architecture: engram is a private, Apache-2.0-grade Go repo (holomush conventions: Task + goreleaser + golangci + lefthook + cog) holding the server (cmd/engram + internal/{auth,store,embed,server}), a generic Helm chart (charts/engram), and CI that publishes a multi-arch image + an OCI Helm chart to ghcr on tag. The cluster’s existing agent-memory ArgoCD Application is converted to multi-source: Source A = the OCI engram chart (workloads), Source B = a trimmed argocd/app-configs/agent-memory kustomize path (cluster ExternalSecrets only). ArgoCD adopts the live qdrant-data PVC by name; nothing is pruned.

Tech Stack: Go 1.26 · go-sdk MCP · go-oidc · Qdrant · Helm (OCI) · ArgoCD multi-source · goreleaser · Task · GitHub Actions · External Secrets · Vault.

Reference: spec docs/engineering/specs/2026-06-01-engram-extraction-design.md (read it verbatim). Conventions source: ~/code/github.com/holomush/holomush (github.com/holomush/holomush).

Source of truth for the moved code: fzymgc-house/selfhosted-cluster@main (#1137, commit 115ba205) — services/memory-mcp/ (incl. internal/auth).


Phase A — Stand up seanb4t/engram (no cluster impact)

Section titled “Phase A — Stand up seanb4t/engram (no cluster impact)”

Task 1: Create repo + move/restructure the Go server

Section titled “Task 1: Create repo + move/restructure the Go server”

Files:

  • Create (operator): GitHub repo seanb4t/engram (private), local clone/worktree.
  • Create: engram/go.mod (module github.com/seanb4t/engram, go 1.26.3)
  • Create: engram/cmd/engram/main.go (from services/memory-mcp/main.go)
  • Create: engram/internal/server/tools.go (from services/memory-mcp/tools.go)
  • Move: engram/internal/{auth,store,embed}/ (verbatim from services/memory-mcp/internal/)
  • Move: engram/Dockerfile (from services/memory-mcp/Dockerfile, drop the services/memory-mcp path assumptions)
  • Step 1: Operator — create the repo and a working clone
Terminal window
gh repo create seanb4t/engram --private --description "Self-hosted, correctable, OAuth-secured memory for coding agents" --clone
cd engram
  • Step 2: Initialize the module and copy the server sources
Terminal window
# from the engram repo root; SC = the selfhosted-cluster checkout
SC=/Volumes/Code/github.com/fzymgc-house/selfhosted-cluster
mkdir -p cmd/engram internal/server
cp "$SC/services/memory-mcp/go.mod" go.mod # then rewrite the module line (next step)
cp "$SC/services/memory-mcp/go.sum" go.sum
cp -R "$SC/services/memory-mcp/internal/auth" "$SC/services/memory-mcp/internal/store" "$SC/services/memory-mcp/internal/embed" internal/
cp "$SC/services/memory-mcp/main.go" cmd/engram/main.go
cp "$SC/services/memory-mcp/tools.go" internal/server/tools.go
cp "$SC/services/memory-mcp/Dockerfile" Dockerfile
  • Step 3: Rewrite the module path + package boundaries

In go.mod, set module github.com/seanb4t/engram. Then rewrite every import prefix across the tree:

Terminal window
grep -rl 'fzymgc-house/selfhosted-cluster/services/memory-mcp' . \
| xargs sed -i '' 's|github.com/fzymgc-house/selfhosted-cluster/services/memory-mcp|github.com/seanb4t/engram|g'

The ENTIRE contents of tools.go move into internal/server/tools.go as package serverdeps, buildDepsFromEnv, every arg type (storeArgs, searchArgs, listArgs, idArgs, updateArgs, scopeArgs), storeMemory, searchMemory, updateMemory, actorFromContext, textResult, and registerTools. Only the entrypoint is exported: rename registerTools(s *mcp.Server)Register(s *mcp.Server); everything else stays unexported within package server. envOr is used by both main.go and tools.go, so move it into internal/server as exported EnvOr(k, def string) string and have cmd/engram/main.go call server.EnvOr.

cmd/engram/main.go stays package main, keeps main() + withAuth() (which imports internal/auth), imports github.com/seanb4t/engram/internal/server, and calls server.Register(s) + server.EnvOr(...). Set the MCP server identity to &mcp.Implementation{Name: "engram", Version: "0.2.0"} (was memory-mcp/0.1.1 — cosmetic handshake identity; tool names are unchanged).

  • Step 4: Verify it builds and existing tests pass
Terminal window
go mod tidy
go build ./...
go vet ./...
go test ./...

Expected: ok for internal/auth (the identity precedence test moves with it), internal/store, internal/embed, and a clean build. No behavior change — this is a relocation.

  • Step 5: Commit

Conventional commit, e.g. feat: import memory MCP server (cmd/engram + internal).


Task 2: holomush conventions (Task, lint, hooks, license, meta)

Section titled “Task 2: holomush conventions (Task, lint, hooks, license, meta)”

Files (adapt from ~/code/github.com/holomush/holomush, trimmed to engram’s needs):

  • Create: engram/Taskfile.yaml, .golangci.yaml, lefthook.yaml, cog.toml, dprint.json, .yamlfmt, .markdownlint.yaml (or .rumdl.toml), .editorconfig
  • Create: engram/.licenserc.yaml, LICENSE (Apache-2.0), LICENSE_HEADER
  • Create: engram/README.md, CONTRIBUTING.md, CLAUDE.md, AGENTS.md
  • Create: engram/.gitignore
  • Step 1: Copy + trim the convention configs

Copy .golangci.yaml, lefthook.yaml, cog.toml, dprint.json, .yamlfmt, .editorconfig, .licenserc.yaml, LICENSE_HEADER, .gitignore from holomush. Remove holomush-specific concerns: no buf/proto, no migrate, no generate:schema, no mockery if unused. Set LICENSE to Apache-2.0 and the LICENSE_HEADER to the Apache short header with Copyright <year> Sean Brandt.

  • Step 2: Author Taskfile.yaml with the applicable target subset
version: "3"
vars:
IMAGE: ghcr.io/seanb4t/engram
tasks:
default: { cmds: [ task: lint, task: test ] }
build: { cmds: ["go build -trimpath -o bin/engram ./cmd/engram"] }
test: { cmds: ["go test ./..."] }
test:coverage:{ cmds: ["go test -coverprofile=cover.out ./..."] }
lint: { deps: [lint:go, lint:yaml, lint:actions, lint:markdown] }
lint:go: { cmds: ["golangci-lint run ./..."] }
lint:yaml: { cmds: ["yamlfmt -lint ."] }
lint:actions: { cmds: ["actionlint"] }
lint:markdown:{ cmds: ["rumdl check ."] }
fmt: { deps: [fmt:go, fmt:dprint, fmt:yaml] }
fmt:go: { cmds: ["gofmt -w .", "goimports -w ."] }
fmt:dprint: { cmds: ["dprint fmt"] }
fmt:yaml: { cmds: ["yamlfmt ."] }
fmt:check: { cmds: ["gofmt -l .", "dprint check"] }
license:check:{ cmds: ["license-eye header check"] }
license:add: { cmds: ["license-eye header fix"] }
chart:lint: { cmds: ["helm lint charts/engram", "helm template charts/engram"] }
chart:push:
cmds:
- "helm package charts/engram --version {{.VERSION}} --app-version {{.VERSION}} -d dist/"
- "helm push dist/engram-{{.VERSION}}.tgz oci://ghcr.io/seanb4t/charts"
requires: { vars: [VERSION] }
release:check: { cmds: ["goreleaser check"] }
release:snapshot:{ cmds: ["goreleaser release --snapshot --clean"] }
deps: { cmds: ["go mod tidy"] }
hooks:install:{ cmds: ["lefthook install"] }
tools: { cmds: ["go install tool"], desc: "installs tools declared via go.mod `tool` directives (Go 1.24+); populate them with `go get -tool <pkg>` per holomush" }
clean: { cmds: ["rm -rf bin dist cover.out"] }
  • Step 3: Apply license headers + verify the toolchain
Terminal window
task license:add
task fmt
task lint
task test

Expected: lint + tests pass; all Go files carry the Apache header. Install any missing tools (golangci-lint, license-eye, dprint, yamlfmt, rumdl, actionlint, goreleaser, helm) per holomush’s tools target.

  • Step 4: Write README.md (real, since Apache-2.0-bound) — what engram is, the MCP tool surface (store_memory/search_memory/list_memory/ get_memory/update_memory/delete_memory/delete_all), the scope convention, the OIDC auth model (issuer/JWKS, actor attribution), and a helm install quickstart with the key values. CONTRIBUTING.md/CLAUDE.md/ AGENTS.md adapted from holomush.

  • Step 5: Commitchore: holomush conventions (Task, lint, hooks, license, docs).


Task 3: Helm chart charts/engram (generic, PVC-identity-preserving)

Section titled “Task 3: Helm chart charts/engram (generic, PVC-identity-preserving)”

Files:

  • Create: engram/charts/engram/Chart.yaml
  • Create: engram/charts/engram/values.yaml
  • Create: engram/charts/engram/templates/_helpers.tpl
  • Create: engram/charts/engram/templates/qdrant.yaml (PVC + Deployment + Service)
  • Create: engram/charts/engram/templates/memory-mcp.yaml (Deployment + Service)

Grounding — current live manifests to port (from selfhosted-cluster/argocd/app-configs/agent-memory/): qdrant.yaml (PVC qdrant-data longhorn-encrypted 5Gi, Deployment qdrant image qdrant/qdrant:v1.18.1 ports 6333/6334, Service qdrant), deployment.yaml (Deployment memory-mcp, env incl. MEM_OIDC_ISSUER, imagePullSecrets: [ghcr-pull-secret], reloader.stakater.com/auto: "true"), service.yaml (Service memory-mcp).

  • Step 1: Chart.yaml
apiVersion: v2
name: engram
description: Self-hosted, correctable, OAuth-secured memory for coding agents
type: application
version: 0.2.0
appVersion: "0.2.0"
  • Step 2: values.yaml — defaults are GENERIC (no fzymgc.house)

Defaults below mirror the live manifests exactly so adoption produces no resource/port drift (the reviewer caught a from-memory qdrant downgrade — these are the real values from argocd/app-configs/agent-memory/{qdrant,deployment}.yaml):

image:
repository: ghcr.io/seanb4t/engram
tag: "" # defaults to .Chart.AppVersion
pullPolicy: IfNotPresent
imagePullSecrets: [] # cluster sets [{ name: ghcr-pull-secret }]
memory:
listenAddr: ":8080"
embed: { model: "ollama/bge-m3", dim: 1024 }
litellm: { url: "", keySecret: { name: "", key: "" } } # cluster supplies
qdrant: { collection: "memory" }
oidc:
issuer: "" # set → enables enforcement (signature/issuer/expiry)
audience: "" # MEM_OIDC_AUDIENCE (optional aud pin)
resourceMetadata: "" # MEM_OIDC_RESOURCE_METADATA (optional; WWW-Authenticate hint)
# Reloader watches the DEPLOYMENT object's annotations, not the pod template.
# Cluster sets { reloader.stakater.com/auto: "true" }.
deploymentAnnotations: {}
resources: { requests: { cpu: 50m, memory: 64Mi }, limits: { cpu: 500m, memory: 256Mi } }
qdrant:
image: qdrant/qdrant:v1.18.1
storage: { size: 5Gi, className: longhorn-encrypted, pvcName: qdrant-data }
# MUST match live (else OOMKill on cutover):
resources: { requests: { cpu: 250m, memory: 512Mi }, limits: { cpu: "1", memory: 2Gi } }
  • Step 3: templates/qdrant.yaml — the PVC MUST render as qdrant-data

The PVC name is taken from .Values.qdrant.storage.pvcName verbatim (no release prefix) so ArgoCD adopts the existing volume:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Values.qdrant.storage.pvcName }} # = qdrant-data (data-safety invariant)
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: {{ .Values.qdrant.storage.className }}
resources: { requests: { storage: {{ .Values.qdrant.storage.size }} } }
---
apiVersion: apps/v1
kind: Deployment
metadata: { name: qdrant }
spec:
replicas: 1
strategy: { type: Recreate }
selector: { matchLabels: { app.kubernetes.io/name: qdrant } }
template:
metadata: { labels: { app.kubernetes.io/name: qdrant } }
spec:
containers:
- name: qdrant
image: {{ .Values.qdrant.image }}
ports: [{ containerPort: 6333, name: rest }, { containerPort: 6334, name: grpc }] # match live names
livenessProbe: { httpGet: { path: /livez, port: rest }, initialDelaySeconds: 10, periodSeconds: 15 }
readinessProbe: { httpGet: { path: /readyz, port: rest }, initialDelaySeconds: 5, periodSeconds: 5 }
volumeMounts: [{ name: data, mountPath: /qdrant/storage }]
resources: {{ toYaml .Values.qdrant.resources | nindent 12 }}
volumes:
- name: data
persistentVolumeClaim: { claimName: {{ .Values.qdrant.storage.pvcName }} }
---
apiVersion: v1
kind: Service
metadata: { name: qdrant }
spec:
type: ClusterIP
selector: { app.kubernetes.io/name: qdrant }
ports:
- { name: rest, port: 6333, targetPort: rest }
- { name: grpc, port: 6334, targetPort: grpc }
  • Step 4: templates/memory-mcp.yaml — reloader on Deployment metadata, full env

The reloader annotation goes on the Deployment metadata.annotations (that is what Reloader watches — not the pod template), image tag defaults to .Chart.AppVersion, OIDC audience/resourceMetadata env are emitted only when set:

apiVersion: apps/v1
kind: Deployment
metadata:
name: memory-mcp
labels: { app.kubernetes.io/name: memory-mcp }
{{- with .Values.memory.deploymentAnnotations }}
annotations: {{ toYaml . | nindent 4 }} # cluster: reloader.stakater.com/auto: "true"
{{- end }}
spec:
replicas: 1
selector: { matchLabels: { app.kubernetes.io/name: memory-mcp } }
template:
metadata: { labels: { app.kubernetes.io/name: memory-mcp } }
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets: {{ toYaml . | nindent 8 }}
{{- end }}
containers:
- name: memory-mcp
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports: [{ containerPort: 8080, name: http }]
env:
- { name: MEM_LISTEN_ADDR, value: "{{ .Values.memory.listenAddr }}" }
- { name: MEM_QDRANT_ADDR, value: "qdrant.{{ .Release.Namespace }}.svc.cluster.local:6334" }
- { name: MEM_QDRANT_COLLECTION,value: "{{ .Values.memory.qdrant.collection }}" }
- { name: MEM_LITELLM_URL, value: "{{ .Values.memory.litellm.url }}" }
- { name: MEM_EMBED_MODEL, value: "{{ .Values.memory.embed.model }}" }
- { name: MEM_EMBED_DIM, value: "{{ .Values.memory.embed.dim }}" }
- name: MEM_LITELLM_KEY
valueFrom: { secretKeyRef: { name: "{{ .Values.memory.litellm.keySecret.name }}", key: "{{ .Values.memory.litellm.keySecret.key }}" } }
{{- with .Values.memory.oidc.issuer }}
- { name: MEM_OIDC_ISSUER, value: "{{ . }}" }
{{- end }}
{{- with .Values.memory.oidc.audience }}
- { name: MEM_OIDC_AUDIENCE, value: "{{ . }}" }
{{- end }}
{{- with .Values.memory.oidc.resourceMetadata }}
- { name: MEM_OIDC_RESOURCE_METADATA, value: "{{ . }}" }
{{- end }}
resources: {{ toYaml .Values.memory.resources | nindent 12 }}
livenessProbe: { tcpSocket: { port: http }, initialDelaySeconds: 10, periodSeconds: 15 }
readinessProbe: { tcpSocket: { port: http }, initialDelaySeconds: 5, periodSeconds: 10 }
---
apiVersion: v1
kind: Service
metadata: { name: memory-mcp }
spec:
type: ClusterIP
selector: { app.kubernetes.io/name: memory-mcp }
ports: [{ name: http, port: 8080, targetPort: http }]
  • Step 5: Lint + render-verify the invariant
Terminal window
task chart:lint
helm template charts/engram \
--set memory.litellm.url=http://x --set memory.oidc.issuer=https://x \
| rg 'kind: PersistentVolumeClaim' -A2

Expected: helm lint clean; the rendered manifest contains a PVC named qdrant-data and Deployments/Services named qdrant and memory-mcp (the adoption invariant). Confirm reloader.stakater.com/auto renders on the Deployment metadata.annotations when memory.deploymentAnnotations is set.

  • Step 6: Commitfeat(chart): engram Helm chart (qdrant + memory-mcp), PVC-identity preserving.

Task 4: CI — goreleaser image + OCI chart on tag

Section titled “Task 4: CI — goreleaser image + OCI chart on tag”

Files:

  • Create: engram/.goreleaser.yaml
  • Create: engram/.github/workflows/ci.yaml
  • Create: engram/.github/workflows/release.yaml
  • Step 1: .goreleaser.yaml — multi-arch (incl. arm64) image

Adapt holomush’s .goreleaser.yaml: a single builds entry for ./cmd/engram (CGO_ENABLED=0, linux/amd64 + linux/arm64), dockers / docker_manifests publishing ghcr.io/seanb4t/engram:{{ .Version }} and :latest as a multi-arch manifest, and a GitHub release. Drop holomush’s proto/extra binaries.

  • Step 2: ci.yaml (PR/push) — quality gates
name: ci
on: { pull_request: {}, push: { branches: [main] } }
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with: { go-version-file: go.mod }
- uses: arduino/setup-task@v2
- uses: azure/setup-helm@v4
- run: task lint
- run: task test
- run: task fmt:check
- run: task license:check
- run: task chart:lint

(Verify each uses: version against the GitHub Marketplace at implementation time — pin to the current major.)

  • Step 3: release.yaml (tag v*) — publish image + OCI chart
name: release
on: { push: { tags: ["v*"] } }
permissions: { contents: write, packages: write }
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with: { fetch-depth: 0 }
- uses: actions/setup-go@v6
with: { go-version-file: go.mod }
- uses: arduino/setup-task@v2
- uses: azure/setup-helm@v4
- uses: docker/login-action@v3
with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- uses: goreleaser/goreleaser-action@v6
with: { args: release --clean }
env: { GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" }
- run: |
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
task chart:push VERSION="${GITHUB_REF_NAME#v}"
  • Step 4: Validate the release config locally
Terminal window
task release:check # goreleaser check
task release:snapshot # builds images locally without pushing

Expected: goreleaser check passes; snapshot produces linux/arm64 + linux/amd64 images.

  • Step 5: Commitci: goreleaser image + OCI Helm chart release on tag.

Task 5: First release — publish v0.2.0 artifacts

Section titled “Task 5: First release — publish v0.2.0 artifacts”

Files: none (operator + CI).

  • Step 1: Push main and tag
Terminal window
git push -u origin main
git tag v0.2.0 && git push origin v0.2.0
  • Step 2: Verify CI published both artifacts
Terminal window
gh run watch -R seanb4t/engram
gh api user/packages/container/engram/versions --jq '.[0].metadata.container.tags' # image tag 0.2.0
helm show chart oci://ghcr.io/seanb4t/charts/engram --version 0.2.0 # chart resolves

Expected: image ghcr.io/seanb4t/engram:0.2.0 (arm64 in the manifest) and chart oci://ghcr.io/seanb4t/charts/engram:0.2.0 both present.

  • Step 3: No commit (artifacts only). Record the published digests in the bead.

Phase B — Cluster credential wiring (in selfhosted-cluster)

Section titled “Phase B — Cluster credential wiring (in selfhosted-cluster)”

Task 6: ArgoCD OCI repo credential + PAT pre-flight

Section titled “Task 6: ArgoCD OCI repo credential + PAT pre-flight”

Files (in selfhosted-cluster):

  • Create: argocd/app-configs/shared-resources/ghcr-seanb4t-charts.yaml
  • Modify: argocd/app-configs/shared-resources/kustomization.yaml (add the new file)

Grounding: mirror argocd/app-configs/shared-resources/ghcr-charts-repo-secret.yaml (the existing ghcr-fzymgc-house-charts ExternalSecret).

  • Step 1: PAT pre-flight (operator) — confirm the existing PAT covers seanb4t
Terminal window
# username/password from Vault secret/fzymgc-house/cluster/ghcr/pull-secret
echo "$PAT" | docker login ghcr.io -u "$USER" --password-stdin
docker pull ghcr.io/seanb4t/engram:0.2.0
echo "$PAT" | helm registry login ghcr.io -u "$USER" --password-stdin
helm pull oci://ghcr.io/seanb4t/charts/engram --version 0.2.0

Expected: both pulls succeed. If they 403, the PAT is org-scoped to fzymgc-house — extend it (or store a new one) before proceeding. This is the blocking gate for the OCI cutover.

  • Step 2: Author ghcr-seanb4t-charts.yaml — copy ghcr-charts-repo-secret.yaml, keep namespace: argocd and the argocd.argoproj.io/secret-type: repository label, change metadata.name + target.name to ghcr-seanb4t-charts, set data.url: ghcr.io/seanb4t/charts, keep type: helm, enableOCI: "true", and the same Vault remoteRef (fzymgc-house/cluster/ghcr/pull-secret).

  • Step 3: Validate + PR

Terminal window
kubectl kustomize argocd/app-configs/shared-resources >/dev/null && echo OK
yamllint -c .yamllint.yaml argocd/app-configs/shared-resources/ghcr-seanb4t-charts.yaml

Open a PR; after merge, confirm ArgoCD created the repository secret:

Terminal window
kubectl -n argocd get secret ghcr-seanb4t-charts -o jsonpath='{.metadata.labels.argocd\.argoproj\.io/secret-type}' # repository
  • Step 4: Commit/PRfeat(argocd): ghcr.io/seanb4t/charts OCI repo credential.

Phase C — Data-safe cutover (in selfhosted-cluster)

Section titled “Phase C — Data-safe cutover (in selfhosted-cluster)”

GATE: Do not start Phase C until hl-73s.25 (Qdrant backup + verified restore) is complete. That is the Phase-0 safety net.

Task 7: Multi-source cutover — adopt the PVC, keep the ExternalSecrets

Section titled “Task 7: Multi-source cutover — adopt the PVC, keep the ExternalSecrets”

Files (in selfhosted-cluster):

  • Modify: argocd/cluster-app/templates/agent-memory.yaml (→ multi-source)
  • Delete: argocd/app-configs/agent-memory/deployment.yaml, qdrant.yaml, service.yaml
  • Modify: argocd/app-configs/agent-memory/kustomization.yaml (keep only the two ExternalSecrets)
  • Step 1: Trim Source B to ExternalSecrets only

Remove deployment.yaml, qdrant.yaml, service.yaml. Edit kustomization.yaml so resources: lists only external-secret.yaml and ghcr-pull-secret.yaml.

  • Step 2: Rewrite agent-memory.yaml to multi-source (mirror temporal-workers.yaml)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: agent-memory
namespace: argocd
finalizers: [resources-finalizer.argocd.argoproj.io]
spec:
project: core-services
sources:
- repoURL: ghcr.io/seanb4t/charts # Source A — workloads
chart: engram
targetRevision: 0.2.0
helm:
releaseName: engram
valuesObject:
image: { repository: ghcr.io/seanb4t/engram, tag: "0.2.0" }
imagePullSecrets: [{ name: ghcr-pull-secret }]
memory:
litellm:
url: "http://litellm.litellm.svc.cluster.local:4000"
keySecret: { name: memory-mcp-litellm, key: litellm_ollama_key }
oidc: { issuer: "https://auth.fzymgc.house/application/o/memory-mcp/" }
deploymentAnnotations: { reloader.stakater.com/auto: "true" }
qdrant: { storage: { className: longhorn-encrypted, pvcName: qdrant-data } }
# qdrant.resources omitted on purpose — chart defaults already match live
# (250m/512Mi req, 1/2Gi limit), so adoption introduces no resource diff.
- repoURL: https://github.com/fzymgc-house/selfhosted-cluster # Source B — cluster ExternalSecrets
targetRevision: HEAD
path: argocd/app-configs/agent-memory
destination: { server: https://kubernetes.default.svc, namespace: agent-memory }
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ServerSideApply=true, CreateNamespace=true, RespectIgnoreDifferences=true]
ignoreDifferences: # carries forward unchanged (ESO drift suppression)
- kind: ExternalSecret
group: external-secrets.io
jqPathExpressions:
- .spec.data[].remoteRef.conversionStrategy
- .spec.data[].remoteRef.decodingStrategy
- .spec.data[].remoteRef.metadataPolicy
  • Step 3: PR + the server-side dry-run GATE (the data-safety check)

Open the PR. Before merge/sync, run a server-side diff against the live cluster from the PR branch’s rendered Application and confirm adoption, not deletion:

Terminal window
# Render the would-be state and server-side diff it:
kubectl diff --server-side -f <(helm template oci://ghcr.io/seanb4t/charts/engram --version 0.2.0 \
--namespace agent-memory --values <cluster-values-from-valuesObject>) 2>&1 | tee /tmp/engram-diff

Expected in the diff: the qdrant-data PVC and both ExternalSecrets show no deletion and no destructive change (PVC spec unchanged → no SSA field-manager conflict); only the Deployments/Services may show benign field-manager/label churn. If anything shows the PVC or an ExternalSecret being deleted, STOP and do not merge.

  • Step 4: Merge + sync, then verify data survived

After merge, watch ArgoCD reconcile the agent-memory app, then:

Terminal window
kubectl -n agent-memory get pvc qdrant-data # still Bound, same volume
kubectl -n agent-memory get secret memory-mcp-litellm # still present (not pruned)
kubectl -n agent-memory get pods # memory-mcp + qdrant Running
kubectl -n agent-memory logs deploy/memory-mcp | rg 'OIDC bearer-token validation enabled'

Then prove the memories survived (via the authenticated memory_oauth route, or a port-forward + an authenticated list_memory): the pre-existing memory 9cc04339-... (the bake-off decision) and 8b00b04a-... are retrievable, and an unauthenticated call still returns 401.

  • Step 5: Commit/PRfeat(agent-memory): cut over to seanb4t/engram OCI chart (multi-source, PVC adopted).

Task 8: Cleanup — remove the relocated server from selfhosted-cluster

Section titled “Task 8: Cleanup — remove the relocated server from selfhosted-cluster”

Files (in selfhosted-cluster):

  • Delete: services/memory-mcp/ (entire directory)

  • Step 1: Confirm Task 7 is verified healthy (memories retrievable, pods Running, 401 enforced) — this is the precondition for deletion.

  • Step 2: Remove the relocated server

Terminal window
rm -rf services/memory-mcp

(The argocd/app-configs/agent-memory/ dir is kept — it is Source B, holding the trimmed ExternalSecrets.)

  • Step 3: Verify nothing else references it
Terminal window
rg -n 'services/memory-mcp|ghcr.io/fzymgc-house/memory-mcp' --glob '!docs/**' . || echo "no references"

Expected: no remaining references outside docs. The old image ghcr.io/fzymgc-house/memory-mcp is no longer deployed (the chart uses ghcr.io/seanb4t/engram).

  • Step 4: Commit/PRchore: remove memory-mcp server (relocated to seanb4t/engram).

  • Two repos: Tasks 1–5 are in seanb4t/engram; Tasks 6–8 are in selfhosted-cluster. Tasks 1–5 must complete (artifacts published) before Task 6, and Task 6 before Task 7.
  • Operator steps: repo creation (Task 1.1), tag push (Task 5), the PAT pre-flight (Task 6.1), and the dry-run gate + sync verification (Task 7.3–4) are human/operator-driven, not pure code edits.
  • Branch protection: all selfhosted-cluster changes go through PRs (jj per references/vcs-preamble.md); never push to main.
  • Out of scope: the curator skill (separate effort), the Tier-0/1 roadmap beads (hl-73s.23.29; they re-target engram but are tracked separately), namespace rename, git history migration.