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(fromservices/memory-mcp/main.go) - Create:
engram/internal/server/tools.go(fromservices/memory-mcp/tools.go) - Move:
engram/internal/{auth,store,embed}/(verbatim fromservices/memory-mcp/internal/) - Move:
engram/Dockerfile(fromservices/memory-mcp/Dockerfile, drop theservices/memory-mcppath assumptions) - Step 1: Operator — create the repo and a working clone
gh repo create seanb4t/engram --private --description "Self-hosted, correctable, OAuth-secured memory for coding agents" --clonecd engram- Step 2: Initialize the module and copy the server sources
# from the engram repo root; SC = the selfhosted-cluster checkoutSC=/Volumes/Code/github.com/fzymgc-house/selfhosted-clustermkdir -p cmd/engram internal/servercp "$SC/services/memory-mcp/go.mod" go.mod # then rewrite the module line (next step)cp "$SC/services/memory-mcp/go.sum" go.sumcp -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.gocp "$SC/services/memory-mcp/tools.go" internal/server/tools.gocp "$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:
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 server — deps, 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
go mod tidygo 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.yamlwith the applicable target subset
version: "3"vars: IMAGE: ghcr.io/seanb4t/engramtasks: 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
task license:addtask fmttask linttask testExpected: 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,actorattribution), and ahelm installquickstart with the key values.CONTRIBUTING.md/CLAUDE.md/AGENTS.mdadapted from holomush. -
Step 5: Commit —
chore: 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: v2name: engramdescription: Self-hosted, correctable, OAuth-secured memory for coding agentstype: applicationversion: 0.2.0appVersion: "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: IfNotPresentimagePullSecrets: [] # 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 asqdrant-data
The PVC name is taken from .Values.qdrant.storage.pvcName verbatim (no
release prefix) so ArgoCD adopts the existing volume:
apiVersion: v1kind: PersistentVolumeClaimmetadata: 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/v1kind: Deploymentmetadata: { 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: v1kind: Servicemetadata: { 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/v1kind: Deploymentmetadata: 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: v1kind: Servicemetadata: { 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
task chart:linthelm template charts/engram \ --set memory.litellm.url=http://x --set memory.oidc.issuer=https://x \ | rg 'kind: PersistentVolumeClaim' -A2Expected: 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: Commit —
feat(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: cion: { 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(tagv*) — publish image + OCI chart
name: releaseon: { 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
task release:check # goreleaser checktask release:snapshot # builds images locally without pushingExpected: goreleaser check passes; snapshot produces linux/arm64 +
linux/amd64 images.
- Step 5: Commit —
ci: 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
mainand tag
git push -u origin maingit tag v0.2.0 && git push origin v0.2.0- Step 2: Verify CI published both artifacts
gh run watch -R seanb4t/engramgh api user/packages/container/engram/versions --jq '.[0].metadata.container.tags' # image tag 0.2.0helm show chart oci://ghcr.io/seanb4t/charts/engram --version 0.2.0 # chart resolvesExpected: 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
# username/password from Vault secret/fzymgc-house/cluster/ghcr/pull-secretecho "$PAT" | docker login ghcr.io -u "$USER" --password-stdindocker pull ghcr.io/seanb4t/engram:0.2.0echo "$PAT" | helm registry login ghcr.io -u "$USER" --password-stdinhelm pull oci://ghcr.io/seanb4t/charts/engram --version 0.2.0Expected: 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— copyghcr-charts-repo-secret.yaml, keepnamespace: argocdand theargocd.argoproj.io/secret-type: repositorylabel, changemetadata.name+target.nametoghcr-seanb4t-charts, setdata.url: ghcr.io/seanb4t/charts, keeptype: helm,enableOCI: "true", and the same Vault remoteRef (fzymgc-house/cluster/ghcr/pull-secret). -
Step 3: Validate + PR
kubectl kustomize argocd/app-configs/shared-resources >/dev/null && echo OKyamllint -c .yamllint.yaml argocd/app-configs/shared-resources/ghcr-seanb4t-charts.yamlOpen a PR; after merge, confirm ArgoCD created the repository secret:
kubectl -n argocd get secret ghcr-seanb4t-charts -o jsonpath='{.metadata.labels.argocd\.argoproj\.io/secret-type}' # repository- Step 4: Commit/PR —
feat(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.yamlto multi-source (mirrortemporal-workers.yaml)
apiVersion: argoproj.io/v1alpha1kind: Applicationmetadata: 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:
# 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-diffExpected 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:
kubectl -n agent-memory get pvc qdrant-data # still Bound, same volumekubectl -n agent-memory get secret memory-mcp-litellm # still present (not pruned)kubectl -n agent-memory get pods # memory-mcp + qdrant Runningkubectl -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/PR —
feat(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
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
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/PR —
chore: remove memory-mcp server (relocated to seanb4t/engram).
Notes for the executor
Section titled “Notes for the executor”- Two repos: Tasks 1–5 are in
seanb4t/engram; Tasks 6–8 are inselfhosted-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-clusterchanges go through PRs (jj perreferences/vcs-preamble.md); never push tomain. - 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.