Recently Written · git

ai-config

One canonical AI agent config (rules, skills, MCP servers) rendered into Claude Code, Codex, Copilot CLI and Antigravity by acsync

git clone https://github.com/equwal/ai-config

Log | Files | Refs


skills/memstate/SKILL.md (15414 bytes)

1 ---
2 name: memstate
3 description: >
4   Persistent, versioned memory via the memstated daemon. Data lives in a
5   single SQLite file on your machine. No cloud, no API key. Use for storing
6   facts, recalling memory, managing projects, and full-text or semantic
7   search of agent summaries. Supports Markdown and direct keypath = value
8   assignment.
9 license: MIT
10 tags:
11   - memory
12   - agent-memory
13   - memstate
14   - versioned-memory
15   - keypath
16 ---
17 
18 # Memstate Memory Management
19 
20 This skill talks to a local memstated daemon over HTTP loopback. The
21 daemon stores data in a SQLite file at `$MEMSTATE_DB` (default
22 `~/.memstate/memstate.db`). No API key. If `MEMSTATE_ADDR` is set, the
23 scripts attach to that daemon. Otherwise each script spawns a private
24 daemon on a random port for the duration of the script and stops it on
25 exit.
26 
27 Scripts live in this skill's `scripts/` directory (installed at
28 `~/.claude/skills/memstate/scripts/`). All examples below assume you
29 run them from there or by absolute path.
30 
31 ## Core Concepts
32 
33 | Concept | Description |
34 |---|---|
35 | **Project** | Top-level container for memories, keyed by `project_id`. Auto-created on first write. |
36 | **Keypath** | Dot-separated path (`decisions.auth_provider`) unique within a project. Stored exactly as you write it. Nothing is auto-prefixed. |
37 | **Memory** | One fact or markdown section stored at a keypath, with full version history. Memory ids are integers. |
38 | **Versioning** | A write to an existing keypath supersedes the old value. The response returns the old version as `superseded`. Writes are synchronous. Data is queryable when the script returns. |
39 | **Tombstone** | A delete adds a tombstone version. History is never destroyed. A new write to the keypath revives it. |
40 
41 ## Naming conventions: follow EXACTLY
42 
43 Every deviation fragments the store into disconnected near-duplicates.
44 
45 - **project_id**: Omit `--project`. Every script derives the default
46   from the git repository name (or the directory basename), slugged to
47   snake_case, so all sessions in one repository share one project
48   automatically. Pass `--project` only to reach a DIFFERENT project,
49   and then only an id that `memstate_get.py --list-projects` lists.
50   Never invent a variant. `my-app`, `myapp`, and `my_app_dev` are three
51   different projects.
52 - **keypath segments**: Lowercase snake_case only (`[a-z0-9_]`), joined
53   by dots. Dates are `YYYY_MM_DD` inside a segment:
54   `task.summary.2026_07_04`. Never `2026-07-04`, camelCase, or spaces.
55 - **keypath shape**: `<area>.<topic>` or `<area>.<topic>.<detail>`.
56   Preferred area segments: `decisions`, `todo`, `notes`, `gotchas`,
57   `questions`, `files`, `config`, `arch`, `task.summary.<date>`.
58 - **One keypath = one fact.** To update a fact, write the SAME keypath
59   with the new value. Versioning keeps the old one. Do not create a
60   sibling keypath for a new value of the same fact.
61 - **Git branches**: Keypaths describe the main/default branch unless
62   stated otherwise. Facts that are only true on an unmerged branch go
63   under `branches.<branch_slug>.<area>...`, with the branch name
64   slugged to snake_case (`feature/foo-bar` →
65   `branches.feature_foo_bar.todo`). When the branch merges, write the
66   durable outcomes to normal top-level keypaths, then recursively
67   delete the `branches.<branch_slug>` subtree. If the branch is
68   abandoned, delete the subtree. Branch-independent knowledge
69   (decisions taken, gotchas, architecture) always lives at the top
70   level.
71 - **category**: The kind of memory. ONE lowercase word from `decision`,
72   `config`, `status`, `note`, `gotcha`, `reference`, `learning`.
73   Filterable in search.
74 - **topics**: Subject tags, lowercase snake_case (`auth,embeddings`).
75   Search matches any listed topic.
76 - **source**: A short provenance string, for example `claude-code
77   session 2026_07_04` or `user decision`. Shown in history.
78 
79 ## User scope: facts that are not about this project
80 
81 Every memory lives in a project by default. Some facts are about the
82 user or the machine and hold in every repository: an installed tool, a
83 path, the Ollama URL, a working preference, the user's role. These go
84 in the reserved project `_user`. Every script reaches it with
85 `--scope user` (never with `--project`).
86 
87 A fact belongs in the user scope only when ALL three hold:
88 
89 1. It stays true if this repository is deleted. It is not about any code.
90 2. It is true in every repository, for this user or for this machine.
91 3. It describes the user or the host, not work. Never a decision, todo,
92    task summary, note, or gotcha about code. Those stay in the project.
93 
94 The daemon enforces this line with a keypath allowlist. A write outside
95 it returns HTTP 400 and nothing is stored. Allowed shapes:
96 
97 | Shape | Meaning | Example |
98 |---|---|---|
99 | `preferences.<topic>` | stated global working preferences | `preferences.commit_style` |
100 | `profile.<topic>` | who the user is; never credentials | `profile.role` |
101 | `host.<host_slug>.env.<topic>` | OS, shell, paths, runtimes, ports | `host.mbp.env.go_bin` |
102 | `host.<host_slug>.tools.<topic>` | how an installed tool is configured | `host.mbp.tools.ollama_url` |
103 
104 `<host_slug>` is the first label of the hostname, slugged like a project
105 id (`Matts-MBP.local` → `matts_mbp`). `memstate_get.py` prints it as
106 `user.host`. Reads prune other machines' `host.*` subtrees; searches with
107 `--scope user` drop them.
108 
109 ```bash
110 python3 scripts/memstate_set.py --scope user --keypath preferences.commit_style \
111   --value "short subjects, no trailers" --category config
112 python3 scripts/memstate_set.py --scope user --keypath host.matts_mbp.tools.ollama_url \
113   --value "http://127.0.0.1:11434, model qwen3-embedding:4b" --category config
114 python3 scripts/memstate_get.py --scope user --keypath preferences --include-content
115 ```
116 
117 Heading extraction (`memstate_remember.py --scope user` without
118 `--keypath`) fits only `## Preferences` and `## Profile`. Host facts need
119 an explicit `--keypath`. One bad section rejects the whole call.
120 
121 Never store secrets, tokens, or credentials in any scope. The
122 denied-prompt rule applies to the user scope too.
123 
124 ## Session project (MCP sessions only)
125 
126 The MCP proxy defaults every call to the repository of its working
127 directory, and that is almost always right. When the prompt is clearly
128 about another subject (for example "set up my nginx config" from the home
129 directory), the agent pins its session once with `project_name` on the
130 first tool call. An existing id pins freely. A new id also needs
131 `new_project=true` and is refused when it looks like an existing id
132 (`regress_tests` vs `regress_test`). One pin per session.
133 
134 One rule for creating projects, enforced by the proxy and by these
135 scripts alike: a write never creates a project unless it targets the git
136 repository you are in, or the call carries `new_project=true` (scripts:
137 `--new-project`). This holds for the cwd project outside a repository, for
138 an explicit project id, and for a soft-deleted project, which the flag
139 revives. A name that resembles an existing project is refused outright;
140 use the existing project, or create the new one with the `memstate` CLI,
141 which is the human escape. The home directory has no default project for
142 writes, and its name is never accepted as a project: pin a project or use
143 the user scope. Ids that start with `_` are reserved; the user scope is
144 `--scope user`, never `--project`, and `--list-projects` does not show it.
145 `--new-project` is an error where nothing can be created (`--scope user`,
146 or the git repository you are in). Reads are never gated. These scripts
147 are one-shot and have no session: pass `--project` instead of
148 `project_name`. A pinned MCP session publishes its pin for the recall hook
149 as `<db dir>/recall/pins/<pid>`, removed when the proxy exits.
150 
151 ## Workflows
152 
153 ### Before starting a task (recall)
154 
155 ```bash
156 # 1. Browse this repo's keypath tree (names only, no content)
157 python3 scripts/memstate_get.py
158 
159 # 2. Read a subtree with content
160 python3 scripts/memstate_get.py --keypath decisions --include-content
161 
162 # 3. Search when you do not know the keypath
163 python3 scripts/memstate_search.py --query "how is authentication configured"
164 python3 scripts/memstate_search.py --query "auth setup" --mode semantic
165 
166 # Other projects: --list-projects shows ids. --project targets one.
167 python3 scripts/memstate_get.py --list-projects
168 ```
169 
170 ### After completing a task (remember)
171 
172 ```bash
173 # One short fact at one keypath
174 python3 scripts/memstate_set.py \
175   --keypath config.port --value "8080" --category config
176 
177 # Markdown summary, split by ## headings (one memory per section).
178 # Each section lands at the top-level <heading_slug> keypath. ###
179 # headings nest one more dot segment. Prose before the first ## lands
180 # at `preamble`. Pass --root notes to nest sections under a prefix, or
181 # --keypath to store ALL content as one memory.
182 python3 scripts/memstate_remember.py \
183   --content "## Decisions\nSwitched JWT -> sessions.\n\n## Gotchas\nCookie must be SameSite=Lax." \
184   --source "claude-code session 2026_07_04" --category note
185 ```
186 
187 Heading names `TODOs`, `Decisions`, `Open Questions`, `Files`,
188 `Notes`, `Gotchas` collapse to the canonical segments `todo`,
189 `decisions`, `questions`, `files`, `notes`, `gotchas`.
190 
191 ### History and cleanup
192 
193 ```bash
194 # How a fact changed over time (newest first, includes tombstones)
195 python3 scripts/memstate_history.py --project my_app --keypath config.port
196 
197 # Tombstone one keypath. History is kept. A new write revives it.
198 python3 scripts/memstate_delete.py --keypath config.old_setting
199 
200 # Tombstone a whole subtree, e.g. a merged branch's state
201 python3 scripts/memstate_delete.py --keypath branches.feature_foo_bar --recursive
202 
203 # Soft-delete a project (any later write to the same id revives it)
204 python3 scripts/memstate_delete_project.py --project my_app
205 ```
206 
207 ## Script reference
208 
209 ### `memstate_set.py`: one fact at one keypath
210 
211 ```bash
212 python3 scripts/memstate_set.py \
213   --keypath KEYPATH --value VALUE \
214   [--project ID] [--new-project] [--source TEXT] [--category WORD] [--topics TAG1,TAG2]
215 ```
216 
217 **Response:** `action` (`created` | `superseded` | `unchanged`),
218 `stored` (the new memory), `superseded` (the prior version, if any).
219 `unchanged` means identical content AND metadata. No new version was
220 written.
221 
222 ### `memstate_remember.py`: markdown or text
223 
224 ```bash
225 python3 scripts/memstate_remember.py \
226   --content "MARKDOWN" \
227   [--project ID] [--new-project]
228   [--keypath KEYPATH]        # store everything as ONE memory here
229   [--root PREFIX]            # heading-split mode: nest sections under this prefix (default: none)
230   [--source TEXT] [--category WORD] [--topics TAG1,TAG2]
231 ```
232 
233 The split is a deterministic parse of `##`+ headings. No LLM, no job
234 queue. The operation is synchronous. **Response:** `method`
235 (`explicit` | `headings`), `items[]` each with `keypath`, `action`,
236 `stored`, `superseded?`.
237 
238 ### `memstate_get.py`: browse and retrieve
239 
240 ```bash
241 python3 scripts/memstate_get.py                               # this repo's tree (names only)
242 python3 scripts/memstate_get.py --keypath KP --include-content
243 python3 scripts/memstate_get.py --list-projects               # all project ids
244 python3 scripts/memstate_get.py --project ID --keypath KP     # another project
245 python3 scripts/memstate_get.py --memory-id N                 # one memory by integer id
246 ```
247 
248 **Response:** the projects list returns `projects[]`. The tree returns
249 `domains[]` and `total_memories`. A subtree returns `memories[]` and
250 `total_count`. The tree gives names only. Pass a keypath (with
251 `--include-content`) to read values.
252 
253 ### `memstate_search.py`: find current memories
254 
255 ```bash
256 python3 scripts/memstate_search.py --query "PLAIN WORDS" \
257   [--project ID]             # default: this repo's project
258   [--all-projects]           # search the whole store
259   [--mode hybrid|fts|semantic]  # hybrid (default) = any word + meaning, fused; fts = every word; semantic = meaning only (needs Ollama)
260   [--threshold 0.0-1.0]      # semantic and hybrid, default 0.5
261   [--category WORD] [--topics TAG1,TAG2]   # topics = match any
262   [--keypath-prefix KP]      # only this keypath or below, e.g. branches.feature_x
263   [--limit N]                # default 20
264 ```
265 
266 Only the current version of each keypath is searchable. Tombstoned
267 keypaths and soft-deleted projects never match. Query text is plain
268 words. Punctuation is safe, and there is no boolean syntax.
269 **Response:** `results[]`, `total_found`, `query`, `mode` (+ `score`
270 per result and `threshold`/`model` in semantic and hybrid modes; hybrid
271 adds `sources` per result and `degraded` when the embedder was
272 unavailable and only FTS hits are present).
273 
274 ### `memstate_history.py`: version chain of one keypath
275 
276 ```bash
277 python3 scripts/memstate_history.py --keypath KP [--project ID]
278 python3 scripts/memstate_history.py --memory-id N
279 ```
280 
281 **Response:** `versions[]` (newest first, includes tombstones),
282 `total_versions`.
283 
284 ### `memstate_delete.py`: tombstone keypath(s)
285 
286 ```bash
287 python3 scripts/memstate_delete.py --keypath KP [--project ID] [--recursive]
288 ```
289 
290 **Response:** `deleted_count`, `deleted_keypaths[]`.
291 
292 ### `memstate_delete_project.py`: soft-delete a project
293 
294 ```bash
295 python3 scripts/memstate_delete_project.py --project ID
296 ```
297 
298 **Response:** `project_id`, `deleted_count`. Reads fail afterwards.
299 Any write to the same project_id revives it with all memories intact.
300 
301 ## Best practices
302 
303 1. **Let the default id work.** Omit `--project`. The repo-derived
304    default keeps every session in one repo on one project. Only pass
305    an explicit id that `--list-projects` shows.
306 2. **Update, do not duplicate.** Same keypath, new value. The version
307    chain is the changelog.
308 3. **Search before browsing.** `memstate_search.py` beats a walk of
309    the tree when you know roughly what you want. The default hybrid
310    mode already matches by meaning; use `--mode fts` when you need every
311    word to match.
312 4. **Only current versions surface.** Search and get return the latest
313    non-deleted version per keypath. Use `memstate_history.py` to see
314    the past. There is no `is_latest` flag to check.
315 5. **Categorize decisions.** `--category decision` on architecture
316    choices makes them retrievable with one filtered search.
317 6. **Keep branch state quarantined.** Put in-flight branch facts under
318    `branches.<slug>.*`. Promote them to the top level on merge, then
319    recursively delete the subtree. Scope a search to one branch with
320    `--keypath-prefix branches.<slug>`.
321 7. **Never save a denied prompt.** A denied prompt is a tool call that the
322    user or a permission check denied. Do not save it under any keypath, in
323    any category, or in any summary. Do not save it as a warning for a later
324    session. No reason overrides this rule.
325 8. **Keep user facts out of projects, and work out of the user scope.**
326    A tool path or a global preference goes to `--scope user`; a
327    decision, todo, or summary never does. The daemon rejects the latter.
328 
329 ## Connecting to the daemon
330 
331 ```bash
332 export MEMSTATE_ADDR="127.0.0.1:8765"     # attach to a shared daemon
333 export MEMSTATE_LOCAL_URL="http://127.0.0.1:9000/api/v1"  # or full URL override
334 ```
335 
336 With neither set, each script spawns its own daemon on a random port
337 against `$MEMSTATE_DB` and stops it on exit. A manually started
338 `memstated --addr HOST:PORT` probes the port first. If another
339 memstated already owns the port, it exits quietly. If an unrelated
340 process owns the port, it exits loudly (code 2).