{"id":50138708,"url":"https://github.com/ctxr-dev/memory","last_synced_at":"2026-05-24T00:01:42.708Z","repository":{"id":356413399,"uuid":"1232389209","full_name":"ctxr-dev/memory","owner":"ctxr-dev","description":"Inspectable local project memory for AI coding agents.","archived":false,"fork":false,"pushed_at":"2026-05-21T02:02:53.000Z","size":1310,"stargazers_count":11,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-21T06:55:29.090Z","etag":null,"topics":["agent","ai","dify","hooks","llm","mcp","memory","rag"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ctxr-dev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-07T22:09:46.000Z","updated_at":"2026-05-11T03:38:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ctxr-dev/memory","commit_stats":null,"previous_names":["ctxr-dev/memory-boilerplate","ctxr-dev/memory"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ctxr-dev/memory","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctxr-dev%2Fmemory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctxr-dev%2Fmemory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctxr-dev%2Fmemory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctxr-dev%2Fmemory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ctxr-dev","download_url":"https://codeload.github.com/ctxr-dev/memory/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ctxr-dev%2Fmemory/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33416315,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T22:14:44.296Z","status":"ssl_error","status_checked_at":"2026-05-23T22:14:43.778Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai","dify","hooks","llm","mcp","memory","rag"],"created_at":"2026-05-24T00:01:42.072Z","updated_at":"2026-05-24T00:01:42.700Z","avatar_url":"https://github.com/ctxr-dev.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003e🧠 Local Dify MCP Memory\u003c/h1\u003e\n\n\u003ch2 align=\"center\"\u003eThe self-learning RAG that makes your AI stop repeating its mistakes\u003c/h2\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eTyped, deduplicated, self-improving project memory for AI coding agents.\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  A local Dify Knowledge stack for high-precision RAG, a stdio MCP bridge for every modern agent client, a two-stage \u003ccode\u003eflush + compile\u003c/code\u003e pipeline that distils sessions into typed atoms instead of dumping transcripts, and a dedicated \u003ccode\u003eself_improvement\u003c/code\u003e dataset where the agent records every correction you give it (and looks up the lesson before related work, so it stops making the same mistake twice).\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/ctxr-dev/memory/actions/workflows/ci.yml\"\u003e\u003cimg alt=\"CI\" src=\"https://github.com/ctxr-dev/memory/actions/workflows/ci.yml/badge.svg\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/ctxr-dev/memory/releases/latest\"\u003e\u003cimg alt=\"Latest release\" src=\"https://img.shields.io/github/v/release/ctxr-dev/memory?display_name=tag\u0026sort=semver\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg alt=\"License: MIT\" src=\"https://img.shields.io/badge/License-MIT-green.svg\"\u003e\u003c/a\u003e\n  \u003cimg alt=\"Local First\" src=\"https://img.shields.io/badge/Local--First-memory-0A7C66\"\u003e\n  \u003cimg alt=\"Dify\" src=\"https://img.shields.io/badge/RAG-Dify-2F6FEB\"\u003e\n  \u003cimg alt=\"MCP\" src=\"https://img.shields.io/badge/MCP-stdio-6E56CF\"\u003e\n  \u003cimg alt=\"Docker Compose\" src=\"https://img.shields.io/badge/Docker-Compose-2496ED\"\u003e\n  \u003cimg alt=\"Node.js\" src=\"https://img.shields.io/badge/Node.js-20+-339933\"\u003e\n  \u003cimg alt=\"Claude Code\" src=\"https://img.shields.io/badge/Claude%20Code-supported-D97706\"\u003e\n  \u003cimg alt=\"Codex/OpenAI\" src=\"https://img.shields.io/badge/Codex-supported-10A37F\"\u003e\n  \u003cimg alt=\"Cursor\" src=\"https://img.shields.io/badge/Cursor-supported-111827\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#install\"\u003eInstall\u003c/a\u003e\n  |\n  \u003ca href=\"#how-memory-is-built\"\u003ePipeline\u003c/a\u003e\n  |\n  \u003ca href=\"#what-gets-saved\"\u003eCategories\u003c/a\u003e\n  |\n  \u003ca href=\"#updates\"\u003eUpdates\u003c/a\u003e\n  |\n  \u003ca href=\"#client-config\"\u003eClients\u003c/a\u003e\n  |\n  \u003ca href=\"STACK.md\"\u003eStack docs\u003c/a\u003e\n  |\n  \u003ca href=\"CONTRIBUTING.md\"\u003eContributing\u003c/a\u003e\n  |\n  \u003ca href=\"SECURITY.md\"\u003eSecurity\u003c/a\u003e\n  |\n  \u003ca href=\"CHANGELOG.md\"\u003eChangelog\u003c/a\u003e\n\u003c/p\u003e\n\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"images/memory-installed.png\" alt=\"Dify Knowledge installed and AI aware of it\" width=\"920\"\u003e\n\u003c/p\u003e\n\n---\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"images/img.png\" alt=\"Dify Knowledge UI showing project memory knowledge bases\" width=\"920\"\u003e\n\u003c/p\u003e\n\n## Why this exists\n\nDumping raw transcripts into a vector store turns a signal-to-noise problem into an embedding-space problem: at scale, retrieval surfaces the noise.\n\nThis boilerplate replaces the dump with a two-stage pipeline:\n\n1. **Flush.** Lifecycle hooks (`PreCompact`, `PostCompact`, `SessionEnd`) call your local LLM (Claude Code CLI by default; Codex, Anthropic, or OpenAI also supported) to extract typed atoms (decisions, bug root causes, feedback rules, lore, references, gotchas) into one `daily-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md` document per flush.\n2. **Compile.** The first `SessionStart` of each new UTC day spawns `compile.mjs` in the background. It reads enabled `daily-*.md` docs, dedup-merges atoms against existing `knowledge-*.md` docs (LLM decides create / update / skip), then disables the source dailies (kept for audit, hidden from search).\n\nMost sessions contribute 0 to 3 small atoms, dedup-merged across history, with metadata that makes retrieval boringly correct.\n\n## Install\n\nThe boilerplate is consumed as `./.memory/src/` inside your project, with its own git history retained for `git pull` updates. Two phases, drive each manually or via an AI prompt:\n\n| Phase | What it does | Manual | AI-driven |\n|---|---|---|---|\n| **1. Host install** | clone, render configs, start Docker stack | [Manual install](#manual-install) | [🤖 AI-driven install](#-ai-driven-install) |\n| **2. Dify onboarding** *(after MCP-client restart)* | API key, dataset slots, metadata schema, optional doc absorb | [Manual flow](#manual-flow) | [🤖 AI-driven flow](#-ai-driven-flow) |\n\n\u003e **Why two prompts?** The MCP server only becomes callable AFTER your client (Claude Desktop, Cursor, Codex) restarts to pick it up. Phase 2 uses MCP tools (`list_datasets`, `create_dataset`, `absorb_files`, ...) that don't exist before that restart, so it can't share a session with Phase 1. Run Phase 1, restart your client, then run Phase 2.\n\n### Prerequisites\n\n- Docker Desktop 4.x+ with Docker Compose 2.24.4+\n- Node 20+ (used at install AND runtime; no `jq` or other extras needed)\n- bash 3.2+, plus standard POSIX utilities (`awk`, `sed`, `grep`, `find`, `mktemp`, `tr`, `cut`)\n- `git`, `curl`\n\n**Cross-platform:** macOS and Linux are first-class. **Windows works via WSL2 or Git Bash:** bootstrap is bash-only and intentionally avoids `jq`, `realpath`, `gsed`, or any other non-portable binary.\n\n\u003cdetails\u003e\n\u003csummary\u003eDocker via Rancher Desktop / Colima (non-standard path)\u003c/summary\u003e\n\nIf your `docker` comes from **Rancher Desktop** (`~/.rd/bin/docker`), Colima, or another non-standard location, the install scripts auto-resolve it: `bootstrap.sh` and `scripts/lib.sh` probe `~/.rd/bin`, `/usr/local/bin`, `/opt/homebrew/bin`, and the Rancher app bundle before giving up, and you can force a specific binary with `DOCKER_BIN=/path/to/docker`. One caveat the scripts can't fix for you: the **Claude Code / MCP-client process** that spawns the memory server runs `docker exec …` from its own environment, and Rancher only adds `~/.rd/bin` to your **interactive** shell PATH (via `.zshrc`/`.bashrc`). If the MCP server fails to start with \"docker: command not found\", ensure your client is launched from a shell that has `~/.rd/bin` on PATH (or symlink `docker` into `/usr/local/bin`).\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003eWindows-specific gotchas\u003c/summary\u003e\n\n- **Line endings**: the repo ships `.gitattributes` forcing LF on shell + Node + config files. If you cloned with `core.autocrlf=true` (Git for Windows default) BEFORE these directives existed locally, run `git add --renormalize . \u0026\u0026 git checkout .` to fix any CRLF in your working tree, otherwise `bash` will choke on `#!/usr/bin/env bash\\r`.\n- **Docker Desktop file sharing**: under Docker Desktop → Settings → Resources, enable the drive (non-WSL) or the WSL2 distro that contains your project. Without this, the workspace bind mounts empty and `scan_documents` / `absorb_files` see no source files.\n- **Symlinks**: the repo ships zero symlinks; do not introduce any locally without enabling Windows Developer Mode (or accept that Git will substitute a 1-line text file for the symlink target).\n\u003c/details\u003e\n\n### Manual install\n\n```bash\n# from inside the project root\ngit clone https://github.com/ctxr-dev/memory ./.memory/src\n./.memory/src/bootstrap.sh --slug \u003cproject-slug\u003e\n./.memory/src/scripts/up.sh    # FIRST RUN IS SLOW: clones the upstream Dify repo\n                          # into .memory/src/vendor/dify, pulls Dify images, and\n                          # builds the bridge. First-run cold pull is 2-5 min\n                          # multi-GB; warm cache is ~30-60s. up.sh prints\n                          # the Dify UI URL\n                          # at the end.\n```\n\n`bootstrap.sh` renders `.agents/` (vendor-neutral, for Cursor / Codex / Claude Desktop / generic MCP clients) and (when `--install-hooks` is on, default) also `.claude/settings.json` (Claude Code hooks) AND `.mcp.json` at the workspace root (Claude Code's project-scope MCP server registration; without this, `/mcp` does NOT see the new memory server even when the bridge is up). It also appends a `/.memory` block to `.gitignore`, detects available LLM CLIs (`claude`, `codex`, falls back to `anthropic` / `openai`), and creates `./.memory/settings/.env` from the template.\n\n**Existing config files are structurally merged, never overwritten:** your hooks, MCP servers, and permissions all pass through verbatim and only boilerplate-owned entries are added or refreshed (see [Updates → Merge contract](#merge-contract)). Re-runs leave `./.memory/settings/.env` untouched.\n\nAfter the stack is up, finish wiring with the [onboarding wizard](#manual-flow) (or the [AI-driven flow](#-ai-driven-flow)).\n\n### 🤖 AI-driven install\n\n\u003e **Phase 1 of 2.** Host-side install only (clone, bootstrap, docker stack up). Run [Phase 2](#-ai-driven-flow) AFTER the stack is up AND your MCP client restarts.\n\nPaste this prompt into your agent (Claude Code, Cursor, Codex) running inside the target project root:\n\n```text\nInstall the local Dify MCP memory boilerplate into this project. Target the current working directory unless I explicitly give you another path.\n\nSteps:\n\n1. Confirm the boilerplate Git URL with me first if you cannot infer it. Default: https://github.com/ctxr-dev/memory\n\n2. Ask me for the project slug. Lowercase ASCII a-z, 0-9, hyphen (e.g. billing-api, docs-site). If I give you a name, propose a sanitised slug derived from the project folder name and confirm. The slug becomes the per-project Docker container, image, and Compose project name, so multiple projects can run their own memory stacks without collisions.\n\n3. Ask me which LLM provider to use for the flush + compile pipeline:\n   - claude (recommended; spawns `claude -p`, no API key needed)\n   - codex (spawns `codex exec --json`, no API key needed)\n   - anthropic (REST with ANTHROPIC_API_KEY in ./.memory/settings/.env)\n   - openai (REST with OPENAI_API_KEY in ./.memory/settings/.env)\n   Detect which CLIs are on PATH before asking. If only one is available, default to it and ask me to confirm.\n\n4. Ask whether to install Claude Code hooks (default: yes). Hooks live in .claude/settings.json and wire SessionStart, PreCompact, PostCompact, SessionEnd, and PostToolUse (matcher ExitPlanMode, for auto-capturing approved plans into the `plans` slot) to ./.memory/src/scripts/hooks/. Other clients can adapt .agents/hooks.json manually.\n\n5. Ask which MCP clients I want registered: Claude Desktop, Cursor, Codex/OpenAI, generic. Note the choices for step 8; the actual snippets only exist after bootstrap.sh runs.\n\n6. Verify host prerequisites or tell me exactly what is missing:\n   - docker (Docker Desktop or engine) with `docker compose` 2.24.4+\n   - node 20+\n   - git, curl, bash 3.2+\n   bootstrap.sh itself only enforces docker + node + docker-compose-version; git and curl are needed by `git clone` and the Dify-version probe. No `jq`, `realpath`, or other extras are required (the install path is intentionally portable to Git Bash on Windows).\n\n7. Run the install. If I chose Codex/OpenAI as a client in step 5 AND the `codex` CLI is on PATH, append `--register-codex` so bootstrap auto-runs `codex mcp add` for me; otherwise tell me to run that command manually after step 8:\n   git clone \u003cboilerplate-git-url\u003e ./.memory/src\n   ./.memory/src/bootstrap.sh --slug \u003cslug\u003e --llm-provider \u003cprovider\u003e [--no-hooks if I declined] [--register-codex if Codex picked]\n\n8. Static verification only (Docker not yet required; the stack is not up yet):\n   bash -n ./.memory/src/bootstrap.sh ./.memory/src/scripts/*.sh ./.memory/src/scripts/hooks/*.sh\n   node --check ./.memory/src/scripts/compile.mjs ./.memory/src/scripts/hooks/flush.mjs ./.memory/src/scripts/hooks/session-start.mjs\n   node --check ./.memory/src/scripts/lib/*.mjs ./.memory/src/mcp-server/src/*.js\n   ( cd ./.memory/src \u0026\u0026 npm test )\n\n   Then print the requested client snippets from `./.memory/src/.agents/clients/` (now that bootstrap has rendered them):\n   ./.memory/src/scripts/mcp-config.sh all\n   For Codex (if not auto-registered in step 7):\n   codex mcp add \u003cslug\u003e-memory -- docker exec -i \u003cslug\u003e-memory node src/index.js\n\n9. Start the stack. WARN ME this is slow on first run: dify-bootstrap clones the upstream Dify repo (~hundreds of MB) and `up.sh` then pulls and builds Dify + the bridge image (2-5 minutes on a cold pull, multi-GB; ~30-60s once the Docker image cache is warm):\n   ./.memory/src/scripts/up.sh\n   (`up.sh` invokes `ui-url.sh` itself, so the Dify UI URL is printed when it finishes.)\n\n10. Tell me the exact next steps after the stack is up:\n    a) Open the printed Dify UI URL.\n    b) Create the admin account, configure an embedding model under Settings -\u003e Model Provider (REQUIRED before any high_quality dataset can be created).\n    c) Open Knowledge -\u003e Service API, create a Knowledge API key.\n    d) Restart your MCP client (Claude Desktop / Cursor / Codex / your terminal-spawned agent) so it picks up the new memory MCP server. The server only becomes callable after this restart.\n    e) Run `./.memory/src/scripts/dify-setup.sh` to wire datasets, install the per-document metadata schema, and (optionally) absorb my existing docs. ALTERNATIVELY paste the second AI prompt from the README (under \"Onboarding -\u003e AI-driven flow\") to a fresh agent session for an MCP-driven walkthrough that uses list_datasets / create_dataset / scan_documents / absorb_files instead of the wizard.\n    f) Final end-to-end smoke (only valid after step e): `./.memory/src/scripts/mcp-smoke.sh` — read-only round-trip across get_memory_config, search_memory (plain + filtered), and recall_lessons.\n\nStop and ask me whenever you would otherwise guess. Do not proceed past any step on assumption. Your config lives in `./.memory/settings/.env` (created from `.memory/src/.env.example`); the wizard (`dify-setup.sh`) manages it. If you must hand-edit, edit `./.memory/settings/.env` (there is no `.memory/src/.env`).\n```\n\n## Onboarding\n\n`dify-setup.sh` is a re-runnable wizard. Once Dify is up and you've configured an embedding model under **Settings → Model Provider** in the UI, it asks at most:\n\n1. **`DIFY_KNOWLEDGE_API_KEY`**: paste it (or skip if already in `./.memory/settings/.env`).\n2. **For each dataset slot** (every `DIFY_DATASET_\u003cNAME\u003e_ID=` line in `./.memory/settings/.env`; defaults: `daily, knowledge, plans, investigations, self_improvement`): auto-create with that name (high_quality + hybrid_search), paste an existing id, or skip.\n3. **Metadata schema**: installs the six per-document fields (`atom_type`, `tags`, `project_module`, `language`, `task_type`, `error_pattern`) on every bound slot, plus optional Dify built-ins (`document_name`, `upload_date`, `last_update_date`).\n4. **Bridge restart**: propagates new env to the MCP bridge.\n5. **Absorb existing docs?**: optional. Scans the workspace, picks files into slots (default `knowledge`), upserts each as `relative_path_with_underscores.md`. Re-running overwrites instead of duplicating.\n\nAdd a slot later by appending a new `DIFY_DATASET_\u003cNAME\u003e_ID=` line to `./.memory/settings/.env` and re-running `dify-setup.sh`; it only asks about new slots.\n\n### Manual flow\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: manual onboarding flow\u003c/summary\u003e\n\n```bash\n./.memory/src/scripts/up.sh           # start Dify + MCP bridge\n./.memory/src/scripts/ui-url.sh       # open the printed Dify UI URL\n                                 # In Dify: admin -\u003e embedding model -\u003e Service API -\u003e create Knowledge API key\n./.memory/src/scripts/dify-setup.sh   # paste key, bind/create slots, optional absorb\n./.memory/src/scripts/mcp-smoke.sh    # validate\n```\n\nAfter upgrading the boilerplate via `git pull`, recreate the bridge so it picks up new env lines:\n\n```bash\n./.memory/src/scripts/up.sh memory_mcp   # rebuilds + recreates only the bridge service\n                                    # (small image, typically \u003c10s)\n```\n\n(A raw `docker compose ... up -d memory_mcp` from the workspace root would fail because Docker Compose can't find `docker-compose.yaml` there; the `./.memory/src/scripts/` wrappers add the correct `-f` flags via `scripts/lib.sh`.)\n\n\u003c/details\u003e\n\n### 🤖 AI-driven flow\n\n\u003e **Phase 2 of 2.** Run [Phase 1](#-ai-driven-install) FIRST, then **restart your MCP client** so the new `\u003cslug\u003e-memory` server is registered. Only paste this prompt after that restart: it uses MCP tools (`list_datasets`, `create_dataset`, `scan_documents`, `absorb_files`, `save_lesson`, `recall_lessons`) that don't exist until then.\n\nPaste the prompt below to your agent (Claude Code, Cursor, Codex with the MCP server registered):\n\n```text\nSet up the Dify memory boilerplate for this project. The MCP server is `\u003cproject-slug\u003e-memory`. Do this:\n\n1. Call `get_memory_config` to confirm DIFY_KNOWLEDGE_API_KEY is set (the bridge surfaces `apiKeyConfigured: true|false` without leaking the key). If false, STOP and tell me to:\n   (a) Open the Dify UI URL printed by ./.memory/src/scripts/ui-url.sh\n   (b) Sign in, configure an embedding model under Settings → Model Provider (REQUIRED before any high_quality dataset can be created)\n   (c) Knowledge → Service API → create a Knowledge API key\n   (d) Paste the key into ./.memory/settings/.env as DIFY_KNOWLEDGE_API_KEY=\u003ckey\u003e\n   (e) Recreate the bridge so the new env is picked up:\n       ./.memory/src/scripts/up.sh memory_mcp\n   THEN re-run me. Do not attempt to proceed without the key — `dify-setup.sh --non-interactive` will exit FATAL.\n\n2. Call `list_datasets` to see what already exists in Dify.\n3. For each of these slots (daily, knowledge, plans, investigations, self_improvement), check whether a dataset with that name already exists.\n   - If it exists, tell me the id and ask whether to bind it.\n   - If it does not, ask whether to call `create_dataset` to create it (high_quality + hybrid_search; requires the embedding model from step 1).\n4. Tell me which DIFY_DATASET_\u003cNAME\u003e_ID values to put in ./.memory/settings/.env, then I will run `./.memory/src/scripts/dify-setup.sh --non-interactive --auto-create` to commit them, OR you tell me the exact lines to paste. The wizard also installs the per-document metadata schema (atom_type, tags, project_module, language, task_type, error_pattern) on every bound slot.\n5. Then call `scan_documents` (default globs cover .md/.mdx/.markdown/.txt/.rst/.adoc) and show me the file list with proposed doc names.\n6. Ask which subset I want absorbed and into which slot (default: knowledge). Use `absorb_files` with `dryRun=true` first, show me the result, and only do the real call after I confirm.\n\n7. Sanity round-trip (proves the metadata schema you installed in step 4 actually works): call `save_lesson` with a deliberately-tagged smoke lesson (title \"Onboarding smoke\", error_pattern \"smoke-test\", project_module \"smoke\", task_type \"unknown\"), then immediately call `recall_lessons(query=\"smoke\", project_module=\"smoke\")`. The lesson must round-trip. If it does NOT, the metadata schema install probably failed; tell me to re-run `./.memory/src/scripts/dify-setup.sh`.\n\n8. Tell me about the cleanup tools available. Three MCP tools handle retracting auto-captured / absorbed docs: `delete_document` (permanent, accepts any slot — use sparingly on lessons / compile-managed slots), `disable_document` (soft, hides from search but keeps audit trail), `enable_document` (reverses a disable). Mention these so I know how to clean up if a plan title changes (the auto-capture writes a new doc under the new slug; the old slug stays unless I explicitly remove it).\n\nStop and ask me whenever you would otherwise guess. This is configuration, not refactoring.\n```\n\n### Saving plans, investigations, or other artefacts manually\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: manual artefact saving\u003c/summary\u003e\n\n`save_to_dataset(dataset, name, text, metadata?)` does upsert-by-exact-name: same `name` overwrites, no duplicates. Iterate freely on a `plan-auth-rewrite.md` and the second save replaces the first. Same applies to absorbed files. The optional `metadata` map applies the per-document Dify fields so the doc is filterable in future `search_memory` and `recall_lessons` calls.\n\n**Plans approved via `ExitPlanMode` are auto-captured** to the `plans` slot by the boilerplate's `PostToolUse` hook (`scripts/hooks/exit-plan-mode.mjs`, invoked via the `exit-plan-mode.sh` wrapper). The doc name is `plan-\u003cslugified-title\u003e.md`, derived from the first H1 in the plan body, so iterating on the same titled plan overwrites the same Dify doc. Tagged `atom_type=plan`, `task_type=planning` (no `project_module` so it doesn't pollute filters; add one via a manual `save_to_dataset` if you want per-module scoping). The hook is a no-op when the user rejects the plan (`tool_response.approved !== true`), when the plan body is empty, when the `plans` slot isn't bound, or when the bridge is unavailable. **The hook only fires once Claude Code reloads `.claude/settings.json`, so after a fresh install or an upgrade you must restart Claude Code before the first plan capture will trigger.** See the [`plan-capture` skill](templates/skills/plan-capture.md) for the agent-facing contract. Investigations remain manual: call `save_to_dataset(dataset=\"investigations\", name=...)` directly until the equivalent capture point exists.\n\n\u003c/details\u003e\n\n## Self-improvement loop\n\nA dedicated `self_improvement` dataset captures lessons learned **only** from negative or corrective user feedback. Agents check it before related work via `recall_lessons`.\n\n### Two MCP entry points\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: recall_lessons and save_lesson contract\u003c/summary\u003e\n\n- **`recall_lessons(query, project_module?, language?, task_type?, error_pattern?, tags?, includeKnowledge?, scoreThreshold?, maxResults?)`** — call BEFORE non-trivial work. Filters `self_improvement` by `atom_type=self-improvement-lesson` plus context. Broadens via fall-back ladder (drops `error_pattern` → `language` → `task_type`) until `min(3, maxResults)` UNIQUE hits or the ladder is exhausted. `project_module` and `tags` are caller-chosen scoping signals and are NEVER dropped. Defaults: `scoreThreshold=0.55`, `maxResults=5`. When `project_module` is set AND `includeKnowledge !== false` (default true), also pulls top `bug-root-cause` + `feedback-rule` from `knowledge` (max 2, appended after lessons, never displacing them).\n- **`save_lesson(title, body, metadata, tags?, evidence?)`** — call IMMEDIATELY when the user corrects you (before replying). Required `metadata.error_pattern` is the dedup key: same `error_pattern` MERGES rather than multiplies in compile. The doc name `lesson-\u003cslug\u003e-\u003cts\u003e.md` matches the format compile recognises, so inline-saved lessons participate in the same dedup-merge pipeline. Available on the next turn.\n\n\u003c/details\u003e\n\n### Two capture paths feed `self_improvement`\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: capture paths\u003c/summary\u003e\n\n1. **Inline (`save_lesson`)**: agent observes correction mid-session and persists immediately. Queryable on the very next turn.\n2. **Flush extraction**: `prompts/flush.md` recognises a `self-improvement-lesson` atom type with the same triggers; lessons missed mid-session are captured at hook boundaries. Compile then routes them to `self_improvement` and dedup-merges by `error_pattern`.\n\n\u003c/details\u003e\n\n### Lesson triggers\n\n| ✅ Save | ❌ Don't save |\n|---|---|\n| Direct correction (\"no\", \"stop doing X\", reverting your work, \"wrong\") | Routine clarification or neutral redirection (\"let's switch to X\") |\n| Repeat correction (\"I told you before\", \"again\", \"same mistake\") | User changing their mind about scope |\n| Wrong-tool / wrong-step / wrong-format | User self-blame (\"oh wait, I gave you the wrong file\") |\n| | Exploration or thinking out loud |\n\n### Metadata schema\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: metadata schema fields\u003c/summary\u003e\n\nSix per-document fields, installed by `dify-setup.sh` on every bound slot (Dify only supports string/number/time, so `tags` is comma-separated, queried with `contains`):\n\n| Field | Used by `recall_lessons` for | Notes |\n|---|---|---|\n| `atom_type` | filter by atom type | one of eight types (seven extracted by flush+compile, plus `plan` set by the ExitPlanMode hook); `self-improvement-lesson` is the lesson key |\n| `project_module` | filter by part-of-codebase | lowercase, hyphenated; `unknown` when unsure |\n| `language` | filter by programming language | empty for language-agnostic lessons |\n| `task_type` | filter by task category | enum: planning, implementation, debugging, refactor, review, deploy, docs, unknown |\n| `error_pattern` | filter and DEDUP by failure mode | required for `save_lesson`; short kebab-case slug like `missing-await`, `bsd-sed-no-arg` |\n| `tags` | fulltext-style fallback | comma-separated list, queried with `contains` |\n\nBuilt-in Dify fields (`document_name`, `upload_date`, `last_update_date`) can be enabled by the wizard for recency-based filtering.\n\n\u003c/details\u003e\n\n### Retrieval contract\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: retrieval contract\u003c/summary\u003e\n\n`search_memory({ query, datasets?, filters?, scoreThreshold?, maxResults? })` applies `filters` as a Dify `metadata_condition` (AND-combined; `tags` uses `contains`, others use `is`) BEFORE the embedding rank, then drops anything below `scoreThreshold`. **Do not load the whole store**: filtered + thresholded retrieval is the contract.\n\n\u003c/details\u003e\n\n## How memory is built\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: architecture diagram\u003c/summary\u003e\n\n```mermaid\nflowchart TB\n  subgraph Capture[\"① Capture (per session)\"]\n    direction TB\n    Hook[\"PreCompact / PostCompact / SessionEnd\"] --\u003e Flush[\"scripts/hooks/flush.mjs\"]\n    Flush --\u003e LLM1[\"LLM extract (typed atoms)\"]\n  end\n\n  LLM1 --\u003e DailyDataset[(\"Dify dataset 'daily'\u003cbr/\u003edaily-\u0026lt;ts\u0026gt;.md\")]\n\n  subgraph Promote[\"② Promote (lazy, once/UTC day)\"]\n    direction TB\n    Start[\"SessionStart\"] --\u003e Compile[\"scripts/compile.mjs\"]\n    Compile --\u003e ReadDaily[\"Read enabled daily-*.md\"]\n    ReadDaily --\u003e LLM2[\"LLM dedup-merge vs 'knowledge'\"]\n  end\n\n  DailyDataset --\u003e ReadDaily\n  LLM2 --\u003e KnowledgeDataset[(\"Dify dataset 'knowledge'\u003cbr/\u003eknowledge-\u0026lt;slug\u0026gt;-\u0026lt;ts\u0026gt;.md\")]\n  LLM2 --\u003e SelfImprovement[(\"Dify dataset 'self_improvement'\u003cbr/\u003elesson-\u0026lt;slug\u0026gt;-\u0026lt;ts\u0026gt;.md\")]\n  Compile --\u003e DisableDaily[\"Disable processed daily-*.md\"]\n\n  subgraph OnDemand[\"③ On-demand (any session)\"]\n    direction TB\n    Absorb[\"MCP absorb_files\"]\n    Save[\"MCP save_to_dataset / save_lesson / write_memory\"]\n  end\n\n  subgraph PlanCapture[\"④ ExitPlanMode auto-capture (per approval)\"]\n    direction TB\n    ExitPlan[\"PostToolUse: ExitPlanMode (approved=true)\"] --\u003e ExitPlanScript[\"scripts/hooks/exit-plan-mode.mjs\"]\n  end\n\n  Absorb --\u003e KnowledgeDataset\n  Save --\u003e AnyDataset[(\"Dify named slots\u003cbr/\u003eplans, investigations, ...\")]\n  Save --\u003e SelfImprovement\n  ExitPlanScript --\u003e PlansDataset[(\"Dify dataset 'plans'\u003cbr/\u003eplan-\u0026lt;slug\u0026gt;.md\")]\n\n  DailyDataset --\u003e Search([\"MCP search_memory / recall_lessons\"])\n  KnowledgeDataset --\u003e Search\n  SelfImprovement --\u003e Search\n  AnyDataset --\u003e Search\n  PlansDataset --\u003e Search\n```\n\n**Everything lives in Dify**, organised by named slots, retrieved via metadata-filtered queries.\n\n- **Named slots**: each `DIFY_DATASET_\u003cNAME\u003e_ID=` line in `./.memory/settings/.env` declares one slot. Defaults: `daily`, `knowledge`, `plans`, `investigations`, `self_improvement`. Add lines to add slots (`DIFY_DATASET_RUNBOOKS_ID=`, ...). No second list to maintain.\n- **Per-atom-type routing**: compile sends `self-improvement-lesson` atoms to `self_improvement` and everything else to `knowledge`. Inline `save_lesson` hits `self_improvement` directly.\n- **Naming inside Dify**:\n  - `daily-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md`: one per flush event (dedup-merged out by compile).\n  - `knowledge-\u003cslug\u003e-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md`: one per deduped fact (compile may write a new version with the same `\u003cslug\u003e` and a new `\u003cts\u003e`, then disable the prior).\n  - `lesson-\u003cslug\u003e-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md`: self-improvement lessons in `self_improvement`.\n  - `\u003crelative_path_with_underscores\u003e.md`: absorbed user docs (`docs/auth/jwt.md` becomes `docs_auth_jwt.md`).\n  - `\u003cyour-name\u003e.md`: anything you upsert via `save_to_dataset` (plans, investigations, decisions). Same name overwrites; iterate freely.\n- **Daily docs are kept after promotion** but disabled (audit trail in UI, hidden from `search_memory`).\n- **No local memory files.** Only on-disk state is `./.memory/src/.compile-state.json` (last compile attempt date).\n- **Recursion guard**: `CLAUDE_INVOKED_BY=memory_compile` prevents compile from triggering its own compile.\n- **Failure modes are explicit**: missing LLM provider, missing Dify keys, or stopped MCP container all cause flush/compile/absorb to skip with a stderr message and exit 0. Hooks never block your session and never write fallback files.\n\n\u003c/details\u003e\n\n## What gets saved\n\nTwo routes: **automatic distillation** (flush + compile) and **on-demand upserts** (absorb + save_to_dataset).\n\n### Atoms extracted by flush+compile\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: atom types table\u003c/summary\u003e\n\nSeven atom types are produced by the flush LLM extractor (`prompts/flush.md`) and routed by compile. Each carries the metadata block (`project_module`, `language`, `task_type`, optional `error_pattern`) plus `tags`. The compile prompt biases toward **update** over **create** when `atom_type`, `project_module`, and (for lessons) `error_pattern` match: same fact never gets written twice; same lesson converges into one canonical document.\n\n| Type | Use when | Routes to |\n|---|---|---|\n| `decision` | \"We chose X over Y because Z.\" Architectural or product choice with rationale. | `knowledge` |\n| `bug-root-cause` | The misleading symptom, the actual cause, and the trap to avoid. (Not the diff: that's in git.) | `knowledge` |\n| `feedback-rule` | A workflow rule the user gave you. Conventions, exit predicates, do/don't. | `knowledge` |\n| `project-lore` | Who's doing what, deadlines, integration quirks not in the code. Decays fast; atoms include dates. | `knowledge` |\n| `reference` | A pointer to a dashboard, runbook, or external project, with the reason to consult it. | `knowledge` |\n| `pattern-gotcha` | A reusable code-level lesson: API quirk, framework footgun, library behavior. | `knowledge` |\n| `self-improvement-lesson` | NEGATIVE OR CORRECTIVE user feedback revealing a behaviour the AI should change next time. | `self_improvement` |\n\n\u003c/details\u003e\n\n### Atoms set by hooks (not extracted from transcripts)\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: hook-set atom types\u003c/summary\u003e\n\n| Type | Set by | Routes to |\n|---|---|---|\n| `plan` | `PostToolUse/ExitPlanMode` hook on approval, or manual `save_to_dataset(dataset=\"plans\", ...)`. The flush extractor is explicitly forbidden from producing this type (see `prompts/flush.md`). | `plans` |\n\n\u003c/details\u003e\n\n### On-demand uploads\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: on-demand upload tools\u003c/summary\u003e\n\nBoth use upsert-by-exact-name (delete-then-create): **same name → updated content; different name → new document**.\n\n| MCP tool | When | Naming + identity |\n|---|---|---|\n| `absorb_files(files[], dataset?, dryRun?)` | Index existing project docs (`docs/**/*.md`, `ARCHITECTURE.md`, RFCs). | `relative/path/with/slashes.md` becomes `relative_path_with_slashes.md`. Re-running overwrites the same Dify document. |\n| `save_to_dataset(dataset, name, text, metadata?)` | Save a plan, investigation, decision record, runbook. | The `name` IS the identity. Polishing the same `plan-auth-rewrite.md` later replaces the prior version. |\n\n\u003c/details\u003e\n\n### MCP tools\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: MCP tool reference\u003c/summary\u003e\n\n| Tool | Purpose |\n|---|---|\n| `search_memory` | Retrieve scored chunks across configured datasets. Accepts `filters` (metadata) + `scoreThreshold` for precise, context-efficient recall. |\n| `recall_lessons` | \"Look before you leap\" entry point. Filters `self_improvement` by inferred task context with broadening fall-back; optionally pulls `bug-root-cause` + `feedback-rule` from `knowledge`. |\n| `get_memory_config` | Inspect bridge configuration without exposing secrets. |\n| `write_memory` / `update_memory` | Create-or-supersede a single document (low-level; compile uses `update_memory`). |\n| `save_to_dataset` | Upsert by exact name with optional `metadata` (durable-artefact path). |\n| `save_lesson` | Sugar over `save_to_dataset` for `self_improvement`; required `metadata.error_pattern` is the dedup key. |\n| `list_datasets` / `create_dataset` | Inspect or create Dify datasets; bind via `dify-setup.sh`. `create_dataset` auto-installs the per-document metadata schema (the six fields). |\n| `delete_document` / `disable_document` / `enable_document` | Clean up an upserted doc by id. `delete_document` is permanent (warns about lessons/compile-managed slots); `disable_document` hides from search but keeps the audit trail; `enable_document` reverses a soft-delete. Use to retract a stale `plan-\u003cold-slug\u003e.md` after a title change, or any auto-captured / absorbed doc you no longer want indexed. |\n| `audit_memory` | Walk the `plans`, `knowledge`, and `self_improvement` slots and return a list of cleanup candidates across four classes: `stale-plans` (slug substring of newer plan, leftover renames), `missing-metadata` (atom-type required fields absent), `stale-project-lore` (older than `MEMORY_AUDIT_LORE_STALE_DAYS`, default 90), `duplicate-error-pattern` (lessons sharing a pattern with a newer canonical). List-only; act via `delete_document` / `disable_document`. |\n| `scan_documents` | Walk the workspace mount; return matches + suggested doc names. The default ignore list (`.git`, `node_modules`, `.venv`, `__pycache__`, `target`, `vendor`, `dist`, `build`, `.next`, `Pods`, `DerivedData`, `_build`, `.terraform`, `.idea`, etc., at any nesting depth) is ALWAYS applied; user `ignore` patterns are added on top, never used as a replacement. `include` defaults to markdown/text; pass `include` to override. |\n| `absorb_files` | Read selected files; upsert each into the chosen dataset. |\n\n\u003c/details\u003e\n\n## Updates\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: upgrade recipe\u003c/summary\u003e\n\n```bash\ncd .memory/src \u0026\u0026 git pull \u0026\u0026 cd .. \u0026\u0026 ./.memory/src/bootstrap.sh --slug \u003cproject-slug\u003e\n./.memory/src/scripts/up.sh memory_mcp   # recreate the bridge so it picks up env changes\n```\n\nRe-running bootstrap is idempotent: `./.memory/settings/.env` is preserved across upgrades; only template-derived files (`.agents/*`, `.claude/settings.json`, `.agents/rules/*`, `.claude/skills/*`) are re-rendered. The bridge reads `./.memory/settings/.env` via Compose's `env_file:`, so any new `DIFY_DATASET_\u003cNAME\u003e_ID=` line takes effect only after a recreate.\n\n**Your config lives in `./.memory/settings/` and survives removing `./.memory/src`.** The canonical `./.memory/settings/.env` (API key + dataset-slot bindings + env knobs) and `./.memory/settings/.dify-version` (the pinned Dify release) live in the gitignored, data-side directory — NOT inside `./.memory/src`. `.memory/src/.env.example` is only the template. Because `./.memory/` also holds your Dify data and is never deleted with `./.memory/src`, you can safely `rm -rf ./.memory/src` (to upgrade clean, or remove the boilerplate) and a later `git clone … ./.memory/src \u0026\u0026 ./.memory/src/bootstrap.sh --slug \u003cslug\u003e` reuses your existing `./.memory/settings/.env` as-is — your API key + bindings stay attached (no `dify-setup.sh` re-run) and the same Dify version is reused. On every bootstrap, new keys added to `.memory/src/.env.example` upstream are auto-merged into your `settings/.env` (existing values untouched). `settings/.env` is `chmod 600`; treat `./.memory/settings/` as secret-bearing. (A pre-0.3.0 install with a legacy `.memory/src/.env` is migrated into `settings/.env` on the next bootstrap, then the legacy file is removed.)\n\n**If you're upgrading across a plan-capture release**, also re-run `./.memory/src/scripts/dify-setup.sh` after `git pull` so the per-document metadata schema gets retro-installed on existing slots. See the callout below for the full upgrade recipe.\n\n\u003e **Upgrading to plan-capture (PostToolUse/ExitPlanMode hook):**\n\u003e\n\u003e Required steps in order:\n\u003e\n\u003e 1. **Re-run `./.memory/src/scripts/dify-setup.sh`** after `git pull`. The wizard's `install_metadata_schema` step is idempotent: it inspects every bound slot, only installs missing fields, and silently skips ones already present. Pre-existing slots created by an OLDER `create_dataset` MCP tool (which did NOT auto-install the schema before this commit) get the six per-document fields retro-installed in seconds. Without this, the new ExitPlanMode hook will succeed in writing plans but log `metadata warning: no fields matched dataset metadata schema` on every save until the wizard runs.\n\u003e 2. **Recreate the bridge container** to pick up env + image changes: `./.memory/src/scripts/up.sh memory_mcp`. The bridge reads `./.memory/settings/.env` only at container start time, so any new `MEMORY_HOOK_EXITPLANMODE_*` knob you added is invisible until restart.\n\u003e 3. **Restart your MCP client** (Claude Code / Cursor / Codex) so it picks up the new `.claude/settings.json` / `.agents/hooks.json` hook entries. Already-running sessions won't fire the new hook until restart.\n\u003e\n\u003e Optional: the `MEMORY_HOOK_EXITPLANMODE_DISABLE` and `MEMORY_HOOK_EXITPLANMODE_MAX_BYTES` knobs are auto-merged into your `./.memory/settings/.env` (commented-out) on the next bootstrap, so you can just uncomment and set them there. If you set them, redo step 2 to refresh bridge env.\n\u003e\n\u003e **Behavior change to be aware of:** `upsertDocumentByName` now reduces same-name documents to one per upsert (closes a concurrent-write race window). If you had transient duplicates from a prior bug, this commit silently merges them on the next upsert. Verify in the Dify UI before relying on the upsert path for non-plan content.\n\n\u003c/details\u003e\n\n### Merge contract\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: merge contract\u003c/summary\u003e\n\n| File class | Behaviour on re-run |\n|---|---|\n| **Mixed-content** (`.claude/settings.json`, `.agents/{hooks,mcp}.json`, `.mcp.json`) | Structurally merged via `scripts/merge-config.mjs` (pure Node, no `jq`). Your existing entries (your own MCP servers, your own hook commands, your `permissions`, your `model`, anything else at the top level) pass through verbatim. Only entries whose `command` carries the boilerplate's signature (`\"$CLAUDE_PROJECT_DIR\"/.memory/src/scripts/hooks/...`) are stripped and re-installed; for `.mcp.json`, only the `\u003cslug\u003e-memory` server entry is owned by the boilerplate. Re-runs are byte-stable when nothing changes. |\n| **Owned-only** (`.agents/clients/*`, `.agents/mcp/\u003cserver\u003e.mcp.json`, `.agents/README.md`) | 100% generated by the boilerplate. Bootstrap REFUSES to overwrite if you have edited them: prints a conflict list and exits non-zero. Either delete the file then re-run, or move your edits elsewhere. |\n| **Skill / rule files** (`.claude/skills/*.md`, `.agents/rules/*.md`) | Always overwritten on re-run; treat them as canonical from the boilerplate. |\n\n\u003c/details\u003e\n\n### What gets committed\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: what gets committed\u003c/summary\u003e\n\n| Path | Tracked | Why |\n|---|---|---|\n| `/memory` | **No** | The cloned boilerplate has its own `.git`. |\n| `/.memory` | **No** | Host-mounted Dify runtime data. |\n| `/.agents`, `/.claude/settings.json`, `/.mcp.json` | **Yes** | Per-project agent config: vendor-neutral hooks/MCP + Claude Code hooks + project-scope MCP server registration. |\n| `./.memory/settings/.env` | **No** | Contains your Dify API key. |\n| `.memory/src/.compile-state.json`, `.memory/src/.compile.lock` | **No** | One-line ops state / transient lockfile. Not memory. |\n\n\u003e **Upgrading from a pre-`pwd -P` checkout (one-time):** if you cloned into a path with a symlink in it (common on macOS with iCloud-synced `~/Documents`, or Linux dev VMs with bind-mounted project trees) AND ran the boilerplate before scripts standardised on `pwd -P`, the next `up.sh` after `git pull` resolves `MEMORY_DATA_DIR` to the **physical** path. Docker treats that as a different bind source, so existing Dify storage volumes appear empty (your data is still on disk under the old, symlink-form path). Run `./.memory/src/scripts/migrate-persistent-data.sh` once to copy state into the resolved location, then `./.memory/src/scripts/up.sh`. Fresh installs are unaffected.\n\n\u003c/details\u003e\n\n## Client config\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: client config\u003c/summary\u003e\n\nGenerated client snippets live under `.agents/clients/` after bootstrap:\n\n```bash\n./.memory/src/scripts/mcp-config.sh all              # print every client snippet\n./.memory/src/scripts/mcp-config.sh codex            # | claude-desktop | cursor\n```\n\nFor Codex/OpenAI:\n\n```bash\ncodex mcp add \u003cproject-slug\u003e-memory -- docker exec -i \u003cproject-slug\u003e-memory node src/index.js\n```\n\nFor Claude Desktop, Cursor, or generic MCP clients, merge `.agents/mcp.json` (or the matching `.agents/clients/\u003cclient\u003e` snippet) into your client's MCP config. Do not paste API keys into client configs; they live only in `./.memory/settings/.env`.\n\nWhen `--install-hooks` is on (default), `.claude/settings.json` is rendered with the four lifecycle events wired to `./.memory/src/scripts/hooks/`. Other clients can adapt `.agents/hooks.json` to their own hook format; see [STACK.md](STACK.md) for the event-to-script table.\n\n\u003c/details\u003e\n\n### Skills + rules\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: skills and rules\u003c/summary\u003e\n\n`bootstrap.sh` renders every `templates/skills/*.md` into BOTH:\n\n- `.claude/skills/\u003cname\u003e.md` (only when `--install-hooks`): Claude Code's project skills directory; auto-loaded.\n- `.agents/rules/\u003cname\u003e.md` (always): vendor-neutral. Cursor / Codex / generic clients can import from here.\n\nToday the boilerplate ships three skills: `self-improvement.md` (the `recall_lessons` + `save_lesson` contract), `plan-capture.md` (how the `ExitPlanMode` auto-capture and manual `save_to_dataset` paths interact for the `plans` slot), and `investigation-capture.md` (when and how to save a long debugging session as a durable artefact in the `investigations` slot; agent-side rule, no hook).\n\n\u003c/details\u003e\n\n## Hook reference\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: hook reference table\u003c/summary\u003e\n\n| Event | Script | Effect |\n|---|---|---|\n| `SessionStart` | `scripts/hooks/session-start.mjs` | Emits an `additionalContext` reminder; lazily spawns compile in the background once per UTC day. |\n| `PreCompact` | `scripts/hooks/flush.mjs pre-compact` | Distils the recent transcript into typed atoms; writes ONE new `daily-\u003cts\u003e.md` doc to the Dify daily dataset. Skips if fewer than `MEMORY_HOOK_PRECOMPACT_MIN_TURNS` turns. |\n| `PostCompact` | `scripts/hooks/flush.mjs post-compact` | Distils Claude Code's `compact_summary` into atoms. Min-turns check bypassed for compact_summary input. |\n| `SessionEnd` | `scripts/hooks/flush.mjs session-end` | Same as PreCompact, with `MEMORY_HOOK_SESSION_END_MIN_TURNS` floor. |\n| `PostToolUse` (matcher `ExitPlanMode`) | `scripts/hooks/exit-plan-mode.mjs` | When the user approves a plan, upserts `plan-\u003cslug\u003e.md` into the `plans` dataset slot (deterministic, no LLM, no timestamp; same title overwrites). Body is redacted + wrapped in an untrusted-content fence. Skips cleanly (exit 0) with a stderr message on rejection, empty plan, oversized plan (`MEMORY_HOOK_EXITPLANMODE_MAX_BYTES`, default 256KB), unbound slot, bridge failure, or `MEMORY_HOOK_EXITPLANMODE_DISABLE=true`. See [`plan-capture` skill](templates/skills/plan-capture.md). |\n\nHook timeouts: 130s for flush hooks (LLM defaults to 120s per call + headroom), 30s for `PostToolUse/ExitPlanMode` (no LLM, but multiple bridge round-trips: find + create + metadata + re-list + dedupe-delete), 15s for `SessionStart` (only emits a reminder + spawns compile detached).\n\n\u003c/details\u003e\n\n## Verification\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: verification tiers\u003c/summary\u003e\n\nEach tier lists its prereqs; stop at the latest one your environment can satisfy.\n\n```bash\n# Tier 1 — Static. Requires: bootstrap.sh only. No Docker, no LLM.\nbash -n ./.memory/src/bootstrap.sh ./.memory/src/scripts/*.sh ./.memory/src/scripts/hooks/*.sh\nnode --check ./.memory/src/scripts/compile.mjs ./.memory/src/scripts/hooks/flush.mjs ./.memory/src/scripts/hooks/session-start.mjs\nnode --check ./.memory/src/scripts/lib/*.mjs ./.memory/src/mcp-server/src/*.js\n\n# Tier 2 — Hermetic unit tests. Requires: node 20+.\n( cd ./.memory/src \u0026\u0026 npm test )\n# (`npm test` invokes `node --test test/*.test.mjs` from inside ./.memory/src/,\n# so the glob is expanded against ./.memory/src/test/. Running it from the parent\n# would fail because the glob expands BEFORE the cd.)\n\n# Tier 3 — Stack health. Requires: up.sh has been run.\n./.memory/src/scripts/ps.sh\n./.memory/src/scripts/ui-url.sh\n\n# Tier 4 — End-to-end MCP smoke. Requires: up.sh + dify-setup.sh + DIFY_KNOWLEDGE_API_KEY + ≥1 dataset bound.\n# Read-only by design: initialize, get_memory_config, plain + filtered search_memory,\n# recall_lessons round-trip with a deliberately-no-match query. Fails with a\n# \"Run dify-setup.sh\" hint if any prereq is missing.\n./.memory/src/scripts/mcp-smoke.sh\n\n# Tier 5 — Entry-point smoke. Requires: bootstrap only.\n# Without bridge + slots + LLM provider, both scripts SKIP gracefully (stderr, exit 0).\n# That's the property we verify here: hooks never block the user's session.\necho '{\"session_id\":\"smoke\",\"hook_event_name\":\"PostCompact\",\"compact_summary\":\"Decision: Dify is the canonical store for project memory.\"}' \\\n  | ./.memory/src/scripts/hooks/post-compact.sh\nnode ./.memory/src/scripts/compile.mjs --dry-run\n\n# Tier 6 — Direct CLI checks. Requires: bridge container running.\n# Verify the metadata schema is installed on the self_improvement slot.\ndocker exec -i \"$(grep '^MCP_CONTAINER_NAME=' ./.memory/settings/.env | cut -d= -f2 | tr -d '\\r')\" \\\n  node src/memory-cli.js list-metadata-fields --datasetId self_improvement\n# expect doc_metadata to include atom_type, tags, project_module,\n# language, task_type, error_pattern.\n\n# Filtered search smoke against the self_improvement slot.\n# (RETRIEVES only; pair with a save_lesson MCP call from your agent for a\n# true save -\u003e recall round-trip.)\ndocker exec -i \"$(grep '^MCP_CONTAINER_NAME=' ./.memory/settings/.env | cut -d= -f2 | tr -d '\\r')\" \\\n  node src/memory-cli.js search --datasetId self_improvement \\\n  --query \"smoke\" --filters '{\"atom_type\":\"self-improvement-lesson\"}'\n```\n\nIf `mcp-smoke.sh` fails with \"No datasets configured\" or \"Flush slot 'daily' has no configured id\", run `./.memory/src/scripts/dify-setup.sh` to bind the slots.\n\n\u003c/details\u003e\n\n### Tier 4.5 — Plan-capture write-path smoke (opt-in)\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: plan-capture write-path smoke\u003c/summary\u003e\n\n`mcp-smoke.sh` is intentionally read-only (no writes that would dirty your dataset). To verify the **ExitPlanMode auto-capture write path** end-to-end against your real Dify:\n\n```bash\n./.memory/src/scripts/plan-capture-smoke.sh           # writes + verifies + deletes a synthetic plan-mcp-smoke-*.md\n./.memory/src/scripts/plan-capture-smoke.sh --keep    # leaves the smoke doc in place for visual inspection\n```\n\nSkips with a clear `SKIP:` message if the bridge isn't running, the `plans` slot isn't bound, or `MCP_CONTAINER_NAME` isn't set in `./.memory/settings/.env`. Use this once after install (or after any upgrade that touches the hook) to prove the full pipeline works against your tenant.\n\n\u003c/details\u003e\n\n## Repository layout (single `./.memory/` folder)\n\nEverything the boilerplate touches lives under one gitignored directory, so your project root gains exactly one entry to ignore (`/.memory`). The `src/` subtree is this cloned repo (safe to `rm -rf` and re-clone); `dify/` and `settings/` are your durable data and survive a re-clone.\n\n\u003cdetails\u003e\n\u003csummary\u003eExpand: repository tree\u003c/summary\u003e\n\n```text\n.memory/                            # single gitignored folder (one .gitignore entry: /.memory)\n├── src/                            # the cloned boilerplate (THIS repo); rm -rf + re-clone safe\n│   ├── bootstrap.sh                # render project-root files; idempotent\n│   ├── compose.mcp.yaml            # Docker Compose override for the MCP bridge\n│   ├── .env.example                # template for ../settings/.env\n│   ├── scripts/\n│   │   ├── up.sh, down.sh, ps.sh   # stack lifecycle\n│   │   ├── ui-url.sh               # discover the host UI port\n│   │   ├── dify-bootstrap.sh       # resolve + pin Dify version, clone vendor\n│   │   ├── dify-setup.sh           # interactive dataset binding + metadata\n│   │   │                           # schema installer + absorb wizard\n│   │   ├── mcp-config.sh           # print client snippets\n│   │   ├── mcp-smoke.sh            # JSON-RPC smoke against the bridge\n│   │   ├── compile.mjs             # daily -\u003e knowledge / self_improvement\n│   │   │                           # promotion (per-atom-type routing,\n│   │   │                           # metadata-filtered dedup-merge)\n│   │   ├── merge-config.mjs        # CLI used by bootstrap.sh to structurally\n│   │   │                           # merge our hooks/MCP entries into the\n│   │   │                           # user's existing config without losing\n│   │   │                           # user content\n│   │   ├── lib/{env,llm,dify-write,redact,slug,datasets,lock,merge-config}.mjs\n│   │   └── hooks/\n│   │       ├── session-start.{sh,mjs}    # lazy compile trigger + reminder\n│   │       ├── pre-compact.sh            # -\u003e flush.mjs pre-compact\n│   │       ├── post-compact.sh           # -\u003e flush.mjs post-compact\n│   │       ├── session-end.sh            # -\u003e flush.mjs session-end\n│   │       └── flush.mjs                 # shared extractor (incl. self-\n│   │                                     # improvement-lesson type + metadata)\n│   ├── prompts/{flush,compile}.md  # LLM extraction + dedup-merge prompts\n│   ├── mcp-server/src/{index,dify,memory-cli,glob,slug}.js\n│   ├── templates/\n│   │   ├── agents/                       # rendered to \u003cproject\u003e/.agents/\n│   │   ├── claude/settings.json          # rendered to \u003cproject\u003e/.claude/\n│   │   ├── skills/self-improvement.md    # rendered to .claude/skills/ AND .agents/rules/\n│   │   ├── skills/plan-capture.md        # rendered to .claude/skills/ AND .agents/rules/\n│   │   ├── skills/investigation-capture.md # rendered to .claude/skills/ AND .agents/rules/\n│   │   └── gitignore.append              # appended to \u003cproject\u003e/.gitignore\n│   └── vendor/dify/                # upstream Dify source, cloned at first dify-bootstrap\n├── dify/                           # Dify persistent data (db, object storage, weaviate, redis)\n└── settings/                       # canonical .env (API key + dataset bindings) + .dify-version\n\n# Memory is stored entirely in Dify, organised by named slot, named:\n#   daily-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md             (one per flush event, daily slot)\n#   knowledge-\u003cslug\u003e-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md  (one per deduped fact, knowledge slot)\n#   lesson-\u003cslug\u003e-\u003cYYYY-MM-DD-HHMMSSmmm\u003e.md     (one per deduped lesson, self_improvement slot)\n```\n\n\u003c/details\u003e\n\nFor deeper Dify configuration, knowledge-base creation, retrieval tuning, persistence, and troubleshooting, see [STACK.md](STACK.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fctxr-dev%2Fmemory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fctxr-dev%2Fmemory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fctxr-dev%2Fmemory/lists"}