o9k-write
Hmem write protocol — picks prefix (L/E/D/P/N/R/I/H…), tree location, and tags, and detects duplicates before persisting. Use before any write_memory or append_memory call (skipping it creates duplicates and misplaced entries), or when the user says 'remember this', 'save this', 'log this', or invokes /o9k-write.
Best use case
o9k-write is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Hmem write protocol — picks prefix (L/E/D/P/N/R/I/H…), tree location, and tags, and detects duplicates before persisting. Use before any write_memory or append_memory call (skipping it creates duplicates and misplaced entries), or when the user says 'remember this', 'save this', 'log this', or invokes /o9k-write.
Teams using o9k-write should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/o9k-write/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How o9k-write Compares
| Feature / Agent | o9k-write | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Hmem write protocol — picks prefix (L/E/D/P/N/R/I/H…), tree location, and tags, and detects duplicates before persisting. Use before any write_memory or append_memory call (skipping it creates duplicates and misplaced entries), or when the user says 'remember this', 'save this', 'log this', or invokes /o9k-write.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# How to use write_memory
Call the MCP tool `write_memory` to save lessons, errors, decisions, or project insights to long-term memory.
If `write_memory` is not available:
1. Tell the user: "write_memory tool not found. Please reconnect the MCP server (in Claude Code: `/mcp`, in other tools: restart the tool)."
2. **NEVER write directly to the .hmem SQLite file via shell commands.** The database has WAL journaling, integrity checks, and tree-structure logic that raw SQL INSERT will bypass — causing corruption or data loss.
---
## Syntax
```
write_memory(
prefix: "E",
content: "Short Title (~50 chars)\n\nL1 body — detailed explanation, can span multiple lines\nsecond body line with more context\n\tL2 node title\n\n\tL2 body text (supports newlines)\n\tmore L2 body\n\t\tL3 detail (2 tabs)\n\t\t\tL4 raw data (3 tabs — rarely needed)"
)
```
**Title + Body convention (git-commit style):** Every node has a **title** (short navigation label) and an optional **body** (detailed content loaded on drill-down). Separate them with a **blank line** — just like a git commit message.
- **Title:** The first line (L1) or first line at a given indent level (L2+). ~50 chars, like a chapter title.
- **Body:** Everything after the blank line at the same indent level. Freetext, no special prefix needed. Shown only when the node is drilled into, not in listings.
- **Legacy `> ` prefix:** Still works for backward compatibility, but blank-line separation is preferred.
- **Without body:** The full text is stored as `content` and the title is auto-extracted from the first `maxTitleChars` characters.
**L1 example with body:**
```
Short Error Title
SQLite connection failed because .mcp.json used a relative path.
The fix was to use an absolute path in the HMEM_PATH env var.
Details about reproduction
Steps: 1. Set HMEM_PATH=./hmem 2. Run hmem serve 3. Observe SQLITE_CANTOPEN
```
**L1 example without body (still works):**
```
SQLite connection failed due to wrong path in .mcp.json
Fix: use absolute path in env var
```
**Indentation:** 1 tab = 1 level. Alternatively: 2 or 4 spaces per level (auto-detected).
**Warning:** A tab at the start of any line always means "go one level deeper" — it is structural, not content. If you need to store code or text that contains leading tabs, use spaces instead.
**IDs and timestamps** are assigned automatically — never write them yourself.
---
## Hashtags — add to every write_memory and append_memory call
Hashtags connect entries **across all prefixes and hierarchy levels**. They are the only cross-prefix discovery mechanism.
**Add 3–5 tags per call (max 10):**
```
write_memory(prefix="E", content="...", tags=["#hmem", "#sqlite", "#bug", "#migration", "#windows"])
append_memory(id="P0029", content="...", tags=["#hmem", "#sync", "#cli"])
```
**Rules:**
- Lowercase, starts with `#`, only letters/digits/hyphen/underscore: `#o9k-sync`, `#api_key`
- `append_memory` tags are **additive** — they do not replace existing tags
- `write_memory` tags: if entry has children → land on **first child node**; if leaf (no children) → land on **root**
- `append_memory` tags are stored **only on the target node** — no upward propagation
- Every node at any depth can have its own tags — use this to make sub-topics discoverable
**Good tags:** `#hmem`, `#sync`, `#sqlite`, `#windows`, `#release`, `#bug`, `#security`, `#althing`, `#cli`, `#migration`
**Bad tags:** `#fix` (too generic), `#important` (no context), `#2026` (not a topic)
**Bulk-tagging** for existing entries:
```
tag_bulk(filter={prefix: "E"}, add_tags=["#bug"]) # all E-entries
tag_bulk(filter={search: "o9k-sync"}, add_tags=["#sync"]) # by full-text search
tag_rename(old_tag="#o9k-store", new_tag="#hmem") # rename a tag everywhere
```
---
## Prefixes
| Prefix | Category | When to use |
|--------|----------|-------------|
| **P** | (P)roject | Project entries — standardized L1 format (see below) |
| **L** | (L)esson | Lessons learned, best practices — cross-project knowledge |
| **E** | (E)rror | Bugs, errors + their fix — auto-scaffolded schema (see below) |
| **D** | (D)ecision | Architecture decisions with reasoning — cross-project knowledge |
| **T** | (T)ask | Cross-project or infrastructure tasks ONLY (see note below) |
| **M** | (M)ilestone | Cross-project milestones ONLY — project milestones go in P-entry L2 "Protocol" |
| **S** | (S)kill | Skills, processes, how-to guides |
| **N** | (N)avigator | Code pointers — where something lives in the codebase |
| **H** | (H)uman | Knowledge about the user — preferences, context, working style |
| **R** | (R)ule | User-defined rules and constraints — "always do X", "never do Y" |
| **I** | (I)nfrastructure | Devices, servers, deployments, network — one entry per device/server |
### Where do tasks, errors, lessons, and decisions go?
**Tasks** belong inside the project's P-entry L2 "Open tasks" node:
```
append_memory(id="P0048.8", content="Implement multi-server sync\n\tPush/pull to all configured servers", tags=["#o9k-sync"])
```
Use the T-prefix ONLY for tasks that span multiple projects or are infrastructure/meta tasks (e.g. "Set up Strato server", "Run curation pass"). These get `links=["P00XX"]` to the most relevant project.
**Milestones** belong in the P-entry L2 "Protocol" node as a chronological entry:
```
append_memory(id="P0048.7", content="v4.0.0 published — project gate + load_project tool (2026-03-27)", tags=["#release"])
```
Use the M-prefix ONLY for milestones that span multiple projects (e.g. "First cross-device sync working").
**Errors (E), Lessons (L), Decisions (D)** stay as independent root entries — they are **cross-project knowledge**. An SQLite lesson learned in hmem applies to every SQLite project. Always add `tags` and `links` to connect them back:
```
write_memory(prefix="E", content="...", tags=["#hmem", "#sqlite"], links=["P0048"])
```
**P-entry "Known issues" (L2)** contains short summaries pointing to E-entries — not the errors themselves:
```
append_memory(id="P0048.6", content="Auto-sync fails with multiple .hmem in CWD → E0097, T0043", tags=["#o9k-sync"])
```
**Custom prefixes:** If none of the above fit, you can use any single uppercase letter. To register it officially (so the system validates it), add it to `hmem.config.json` under `"prefixes"`:
```json
{ "prefixes": { "R": "Research" } }
```
Custom prefixes are merged with the defaults — they don't replace them. Without registering, the system will reject the prefix.
### Schema-Enforced Entries (P and any prefix with a defined schema)
The MCP server enforces schemas for any prefix that has a `schemas` entry in `hmem.config.json`.
For those prefixes, two rules apply:
1. **`write_memory`**: L2 node names must match the defined section names (error otherwise).
2. **`append_memory` to root entry (e.g. `P0029`, no dot)**: blocked for arbitrary content — you cannot
add new L2 sections outside the schema. **Exception:** if the first line of content is a valid schema
section name (e.g. `"Rules"`), the append is allowed — useful for adding a missing section to an
existing entry. Otherwise append to a specific section: `append_memory(id="P0029.N", content="...")`.
By default, `P` has a schema. If `L`, `D`, or other prefixes are configured with schemas, the same rules apply.
### P-Entry Standard Schema
Every project entry MUST follow this structure.
**L1 Title:** `Name | Status | Tech Stack | GH: owner/repo | Short description`
The GH field is optional — include it when a GitHub repo exists, omit otherwise.
**L1 Body:** (same line or next non-indented line) One-sentence project summary.
**Status values:**
| Status | Meaning |
|--------|---------|
| New | Just started, concept phase |
| Active | In active development |
| Mature | Feature-complete, only bugfixes |
| Paused | On hold, will resume later |
| Archived | Done or abandoned |
**L2 categories (fixed order, skip sections that are empty):**
The MCP server validates that L2 nodes start with one of these names. Minimum for a new project: Overview + Codebase (or Usage).
| L2 Category | What goes here | L3 children |
|-------------|---------------|-------------|
| **Overview** | First thing an agent reads (like CLAUDE.md /init) | Current state, Goals, Architecture, Environment |
| **Codebase** | Code structure — NO code, only names + signatures | Entry point, Core modules (each module = L4 node with signature + purpose + return), Helpers, Config, Tests |
| **Usage** | How the project is used | Installation/Setup, CLI/API commands, Common workflows |
| **Context** | Background and motivation | Initiator, Target audience, Business context, Dependencies (links) |
| **Deployment** | Build/CI/CD/publish process | (flat or with L3 sub-steps) |
| **Bugs** | Active bugs + known limitations | L3: inline report (symptom + cause) OR pointer to E-entry (`→ E0097`). L4: reproduction steps |
| **Protocol** | Session log, chronological | One-liner per session + links to O-entries |
| **Open tasks** | Project-specific TODOs | One per L3 node. Cross-project tasks → T-prefix with links |
| **Ideas** | Feature ideas, brainstorming | L3: short description, L4: implementation details |
**load_project tool:** Use `load_project(id="P0048")` to activate a project and get the full briefing (L2 content + L3 titles) in one call. This is the recommended way to start working on a project — it combines read + activate.
**Markers you may see on entries:**
| Marker | Meaning |
|--------|---------|
| `[♥]` | Favorite — always expanded in bulk reads |
| `[★]` | Top-accessed — high weighted access score |
| `[≡]` | Top-subnode — many children |
| `[⚡]` | Task-promoted — relevant to an active T/P/D entry (tag overlap) |
| `[*]` | Active — currently in focus |
| `[P]` | Pinned — super-favorite, shows full L2 |
| `[!]` | Obsolete — superseded, kept for history |
| `[-]` | Irrelevant — hidden from bulk reads |
| `✓` | Synced — backed up to all sync servers |
**Complete P-entry example (WeatherBot):**
```
write_memory(
prefix="P",
content="WeatherBot | New | Python/Discord.py | GH: user/weatherbot\n\nDiscord bot for weather forecasts — slash commands for current weather and multi-day forecasts\n\tOverview\n\t\tCurrent state\n\n\t\tScaffolding done, no commands yet. Bot connects to Discord but has no slash commands registered.\n\t\tGoals\n\n\t\tDaily/hourly forecasts via slash commands, multi-city support, embed formatting\n\t\tArchitecture\n\n\t\tDiscord slash command → OpenWeatherMap API → formatted embed. Single-file cog pattern.\n\t\tEnvironment\n\n\t\t/home/user/weatherbot, python bot.py, needs DISCORD_TOKEN + WEATHER_API_KEY in .env\n\tCodebase\n\t\tEntry point — bot.py, start: python bot.py\n\t\tCore modules\n\t\t\tweather_cog.py — WeatherCog(Cog); fetch_forecast(city: str) → discord.Embed\n\t\t\tformatter.py — format_embed(data: dict) → discord.Embed\n\t\tHelpers / Utilities\n\t\t\tapi_client.py — get_weather(city: str) → dict; wraps HTTP to OpenWeatherMap\n\t\tConfig / Constants — .env: DISCORD_TOKEN, WEATHER_API_KEY, DEFAULT_CITY\n\t\tTests — pytest, test_weather_cog.py (3 tests)\n\tUsage\n\t\tInstallation / Setup — pip install -r requirements.txt, cp .env.example .env\n\t\tCLI / API — /weather <city>, /forecast <city> (planned)\n\tContext\n\t\tInitiator — personal project, Mar 2026\n\t\tTarget audience — personal Discord server\n\t\tDependencies — discord.py, OpenWeatherMap API, aiohttp\n\tOpen tasks\n\t\tImplement /forecast command\n\n\t\tMulti-day view with daily highs/lows and weather icons per day\n\t\tAdd city autocomplete",
tags=["#discord", "#python", "#weather", "#bot"],
links=[]
)
```
Note: L2 nodes use 1 tab, L3 uses 2 tabs, L4 uses 3 tabs. Separate title from body with a blank line at the same indent level. Skip empty sections — no need for placeholder text.
### Auto-Scaffold for Schema Prefixes (v7.0.0+)
Any prefix with a schema defined in `hmem.config.json` gets its sections **auto-created on write** when you don't supply L2 nodes. This includes `I`, `A`, `P`, and any custom prefix with a schema. The write response shows the full section list:
```
write_memory(prefix="I", content="Laptop | Active | macOS 14 | macbook", tags=["#device"])
→ Schema: .1 Specs, .2 OS, .3 IP Address, .4 Access, .5 Services, .6 Apps, .7 Storage, .8 Notes
```
Fill sections afterwards with `append_memory(id="I00XX.1", content="...")`. If you supply L2 nodes yourself, they are validated against the schema but no additional nodes are created.
### E-Entry Schema (auto-scaffolded)
E-entries have a **pre-built structure** — just provide a title and short description, the server creates the rest:
```
write_memory(prefix="E", content="hmem sync bug on v1.0.1\n\nConnection fails when HMEM_PATH contains spaces", tags=["#hmem", "#sync", "#path"])
```
This auto-creates:
- **.1 Analysis** (your description goes here automatically)
- **.2 Possible fixes**
- **.3 Fixing attempts**
- **.4 Solution**
- **.5 Cause**
- **.6 Key Learnings**
Plus `#open` tag. Fill in the nodes as you debug with `append_memory`/`update_memory`. The response shows **similar E/D entries by tag overlap** — check them before reinventing the wheel. When solved, replace `#open` with `#solved` and fill .4 + .5 + .6.
E-entries are **not shown in bulk reads** — they surface automatically via tag overlap when you create new E/D entries. Solved bugs are knowledge, not clutter.
### Marking entries as favorites
Mark any entry as a favorite to ensure it always appears with its L2 detail in bulk reads (alongside a `[♥]` marker). Use this for reference info you need to see every session — API endpoints, key decisions, frequently looked-up patterns.
```
write_memory(prefix="D", content="...", favorite=true) # set at creation
update_memory(id="D0010", content="...", favorite=true) # set on existing
update_memory(id="D0010", content="...", favorite=false) # clear
```
Favorites are **not** a prefix — they are a flag on any entry regardless of category.
Use sparingly: if everything is a favorite, nothing is. Prefer high-value reference entries over fleeting notes.
---
### Marking entries as obsolete
When you notice that an entry is outdated — superseded by a newer approach, a fixed bug, or changed architecture — do **not** delete it. Mark it as obsolete with a correction reference:
```
# Step 1: Write the correction FIRST
write_memory(prefix="E", content="Correct approach is XYZ\n\tDetails...") # → E0076
# Step 2: Mark old entry obsolete — MUST include [✓ID] tag
update_memory(id="E0023", content="Wrong approach — see [✓E0076]", obsolete=true)
```
**The `[✓ID]` tag is enforced.** The system will reject `obsolete=true` without a correction reference. This ensures every obsolete entry points to its replacement. The system also creates **bidirectional links** automatically (E0023↔E0076).
The entry stays in memory with a `[!]` marker. Past errors still carry learning value ("we tried this and it failed because..."). The curator may eventually prune it, but that's their decision, not yours.
**Shortcut for stale entries:** If no correction exists (entry is just old/irrelevant, not wrong), only the curator can mark it obsolete without `[✓ID]`.
---
### N — Navigator (Code Pointers)
Use `N` to save a pointer to a specific file, function, or code location so you don't have to search for it next session.
```
write_memory(
prefix="N",
content="Link-Auflösung beim read_memory-Aufruf
src/o9k-store.ts ~line 269 — read() method, ID branch
Guard: resolveLinks !== false prevents circular refs
Introduced in v1.4.0",
links=["E0069"]
)
```
**L1:** What it is — one sentence describing the concept/feature
**L2:** Exact file path + line range + function/method name
**L3:** Context, caveats, related patterns
**Links:** Related entries (errors, decisions, lessons)
**Your responsibility:** Update your N entries whenever you notice code has moved or logic has changed. You don't need the curator for this — use `update_memory` directly. Stale pointers are worse than none. If you cannot verify whether the pointer is still valid, mark it obsolete: `update_memory(id="N0012", content="...", obsolete=true)`.
---
## Title + Body Quality Rules
**Title:** Short navigation label, ~50 chars (configurable via `maxTitleChars`). Think "chapter title in a book".
- Good: `"hmem.py Performance: Bulk-Queries statt N+1"`, `"Ghost Wakeup Bug in msg-router.ts"`
- Bad: `"Fixed a bug"`, `"Important lesson"` (too vague)
**Body (after blank line):** Detailed explanation — full sentences, multiline OK. Shown on drill-down, hidden in listings.
- Must be understandable without any context
- Not "Fixed a bug" — instead explain root cause, fix, and impact
**With title + body (recommended):**
```
write_memory(prefix="L", content="hmem.py Performance: Bulk-Queries statt N+1\n\nAlle Nodes in 2 Bulk-Queries laden, nicht pro Entry einzeln.\nVorher: load_nodes() pro Entry = N+1 SQLite-Connections.\n\tImplementation detail\n\n\tChanged read() to batch-fetch all nodes for visible entries in one query")
```
**Without body (simple entries, backward-compatible):**
```
write_memory(prefix="E", content="SQLite connection failed due to wrong path in .mcp.json\n\tFix: use absolute path in env var")
```
Title auto-extracted: `"SQLite connection failed due to wrong path in .mc"`
---
## Company Knowledge (requires AL+ role)
```
write_memory(
prefix: "S",
store: "company",
content: "..."
)
```
---
## Before Writing: Navigate the Tree First
**Never write blindly.** Before creating a new entry or appending, navigate the existing tree top-down to find the correct insertion point. New information almost always belongs inside an existing entry — not as a new root.
### Protocol
**Step 1 — Check L1 summaries (already in context)**
Scan the root entries visible in your context. Is there a matching root for this topic?
- **No match** → `write_memory()` creates a new root
- **Match found** → continue to Step 2
**Step 2 — Read the matching root's children**
```
read_memory(id="P0029") # shows root + all L2 titles
```
Do any L2 titles match the sub-topic?
- **No match (and no schema)** → `append_memory(id="P0029", content="...")` adds a new L2
- **No match (schema-constrained entry like P)** → find the closest existing section and append there (arbitrary L2 additions are blocked; only valid schema section names are allowed at root)
- **Match found (e.g. .15)** → continue to Step 3
**Step 3 — Drill into that L2**
```
read_memory(id="P0029.15") # shows L2 node + all L3 titles
```
Does an L3 match even more specifically?
- **No match** → `append_memory(id="P0029.15", content="...")` adds a new L3
- **Match found** → continue drilling (Step 4: `read_memory(id="P0029.15.2")`, etc.)
**Stop drilling when:** no child matches, or you've reached the level of granularity that fits.
### Example
New insight about MCP server restart behavior after TypeScript compile:
```
# Step 1: L1 summaries show L0074 "MCP server muss nach Kompilierung neugestartet werden"
# Step 2: read_memory(id="L0074") → shows .1 "Fix: kill + auto-respawn", .2 "Context: Althing only"
# No L2 matches the new sub-case → append at L2
append_memory(id="L0074", content="Standalone o9k-mcp: npm restart required (no auto-respawn)")
# → adds L0074.3 (L3 under the existing lesson)
```
Instead of: `write_memory(prefix="L", content="MCP restart needed after compile...")` — which would duplicate L0074.
### When `write_memory` is correct
Only use `write_memory` when:
- No root entry exists for this topic at all
- The topic is genuinely orthogonal (different error, different decision, different project)
- You're creating an E/L/D entry for a new root cause, not an extension of an existing one
**Rule:** If in doubt, drill one level deeper before deciding to create a new root.
---
## When to save?
**Checkpoint mode matters.** Check `checkpointMode` in hmem.config.json:
- **`"auto"` (recommended):** A background Haiku subagent handles checkpoints automatically every N exchanges. It reads recent O-entry exchanges, calls `read_memory` to avoid duplicates, and writes L/D/E entries + handoff via MCP tools. It also writes a rolling checkpoint summary (`[CP]` node tagged `#checkpoint-summary`) that compresses older exchanges for `load_project`. Skill-dialog exchanges are auto-tagged `#skill-dialog` and filtered from context injection. **You do NOT need to write entries yourself** unless the user explicitly asks you to save something specific.
- **`"remind"`:** You will receive a CHECKPOINT reminder every N messages. When you see it, save key learnings yourself using `write_memory` / `append_memory`.
**In both modes:** Only save what is still valuable in 6 months.
| Save | Don't save |
|------|-----------|
| New root cause + fix | Routine actions without learning value |
| Insight that changes future work | What's already in the codebase |
| Architecture decision + reasoning | Temporary debugging notes |
| Unexpected tool/API behavior | What's in the documentation |
One `write_memory` call per category — entire hierarchy in one `content` string.
---
## Updating Existing Memories
Use `update_memory` and `append_memory` to modify entries without deleting and recreating them.
### update_memory — Fix outdated text
Updates the text of a single node. Children are **not** touched.
```
update_memory(id="L0003", content="Corrected L1 summary — new wording")
update_memory(id="L0003.2", content="Fixed L2 detail")
update_memory(id="D0010", content="New L1", links=["E0042"]) # also update links
```
Use when: the wording is wrong, outdated, or needs clarification.
### append_memory — Add detail to existing entry
Appends new child nodes under an existing root or node. Existing children are preserved.
Content indentation is **relative to the parent** — 0 tabs = direct child of `id`.
Body works the same as in `write_memory` — blank line separates title from body.
> **Schema enforcement:** For entries with a defined schema (e.g., P, I), appending to the root
> (e.g., `id="P0029"`) is **blocked** unless the content's first line matches a defined section
> name. Use case: adding a missing section (e.g. `"Rules"`) to an entry created before that
> section existed. For arbitrary new content, target a specific section:
> `append_memory(id="P0029.3", content="...")`.
> For entries without a schema (L, D, E, etc. by default), root appends are always allowed.
```
append_memory(
id="L0003",
content="New finding discovered later\n\nDetailed explanation of what was found and why it matters.\nThis can span multiple lines.\n\tSub-detail about it"
)
# → adds L0003.N (L2 with title + body) and L0003.N.1 (L3)
# ↑ only works if L has no schema defined; use L0003.N for schema-constrained entries
append_memory(
id="P0029.3",
content="New detail in the Usage section"
)
# → adds P0029.3.M (L3 under section .3) — correct way for schema-constrained entries
append_memory(
id="L0003.2",
content="Extra note under L0003.2"
)
# → adds L0003.2.M (L3)
```
Use when: you have new context to add without replacing what's there.
### When to use which
| Situation | Tool |
|-----------|------|
| L1 wording is wrong/outdated | `update_memory` |
| A sub-node has wrong detail | `update_memory` |
| You have new info to add | `append_memory` |
| Entry is completely wrong | mark obsolete with `[✓newId]`, then `write_memory` for the correction |
---
## Access Count (Automatic + Time-Weighted)
Access counts are managed automatically — every `read_memory` and `append_memory` call bumps the accessed entries. The ranking uses **time-weighted scoring** (`access_count / log2(age_in_days + 2)`) so newer entries with fewer accesses can outrank stale old ones. Entries with the highest weighted scores get `[★]` markers and expanded treatment in bulk reads. To explicitly mark an entry as important, use `favorite: true` on `write_memory` or `update_memory`.
---
## Bulk Tag Operations
Apply tags to multiple entries at once, or rename a tag everywhere:
```
# Add #bugfix to all E-prefix entries
tag_bulk(filter={prefix: "E"}, add_tags=["#bugfix"])
# Add tag to entries matching a search term
tag_bulk(filter={search: "FTS5"}, add_tags=["#search", "#sqlite"])
# Remove #old from all entries that have it
tag_bulk(filter={tag: "#old"}, remove_tags=["#old"])
# Add and remove simultaneously
tag_bulk(filter={prefix: "L", tag: "#draft"}, add_tags=["#stable"], remove_tags=["#draft"])
# Rename a tag everywhere
tag_rename(old_tag="#o9k-store", new_tag="#hmem")
```
Use `tag_bulk` when adding a new systematic tag to an existing category, or cleaning up after a tagging convention change. `tag_rename` handles typos or renames across the entire memory.
---
## H-Prefix: User Skill Assessment
Actively track the user's expertise level across topics. This drives how you communicate —
a coding expert doesn't need variable explanations, a beginner doesn't need jargon.
### Structure
One H-entry per main topic, with sub-nodes per subtopic:
```
write_memory(prefix="H", content="User Skill: IT
Coding — Advanced: writes TypeScript fluently, debugs SQLite schemas, understands async/MCP
Terminal/CLI — Advanced: bash, git, systemctl, nvm, sqlite3 comfortable
Networking — Intermediate: HTTP/DNS solid, asked about WebSocket details
DevOps — Intermediate: systemd + nvm yes, Docker unfamiliar",
tags=["#skill-assessment", "#it"])
```
Levels: **1-10 scale** (see user-assessment skill for full details).
1-2 = no experience, 5-6 = intermediate, 9-10 = expert. Half-points allowed.
Always include evidence (observed behavior, not assumptions).
### When to assess
- **First interaction**: Make initial assessment from vocabulary, questions, and tool usage
- **Ongoing (every few exchanges)**: Watch for signals:
- **Upgrade signals**: uses domain-specific terms correctly, solves problems independently, corrects the agent
- **Downgrade signals**: "das verstehe ich nicht", "explain that", asks about basic concepts, misuses terms
- **On /save**: Review and update assessments if evidence accumulated
### How to update
Reference the O-entry (automatic session log) where the skill change was observed:
```
# User demonstrated new skill — link to the exchange that proves it
append_memory(id="H0010", content="Docker — Intermediate: configured docker-compose independently (see O0042.15)")
# User's skill improved
update_memory(id="H0010.3", content="Networking — Advanced: configures DNS, TLS, reverse proxies (see O0042.23)")
# User struggled — downgrade with evidence
update_memory(id="H0010.4", content="DevOps — Beginner: asked what systemd is, needed step-by-step (see O0042.8)")
```
The O-entry reference lets future agents verify the assessment by reading the original conversation.
### How to USE assessments
Before explaining anything technical, check the relevant H-entry:
- **Beginner**: Explain concepts, use analogies, avoid jargon, step-by-step
- **Intermediate**: Brief explanations, some jargon OK, link to docs for details
- **Advanced**: Direct technical language, skip basics, focus on trade-offs
- **Expert**: Peer-level discussion, challenge assumptions, discuss edge cases
Example: If H0010.1 says "Coding — Advanced", don't explain what a Map is.
If H0010.4 says "DevOps — Beginner", explain what a systemd service does before configuring one.
### Topics are open-ended
Not just IT — any domain the user works in:
- Music (theory, instruments, production)
- Mechanical (bikes, cars, tools)
- Business (accounting, marketing, management)
- Languages (German, English proficiency)
Create new H-entries as topics emerge naturally from conversation.
---
## Language Consistency
Match the language of existing entries. Before writing, check what language the memory store uses (run `read_memory()` if unsure). If existing entries are in German, write in German. If English, write in English. Do not mix languages within a single store — it makes search and curation harder.
## After Writing: Verify the Entry Captured the Insight
For L, D, and E entries with complex reasoning, do a quick read-back:
```
read_memory(id="X")
```
Ask: does this entry answer **"why?"** not just "what?"
Update if any of these are true:
- Entry is under 20 words with no body
- Content is "Lesson learned: X" with no explanation or evidence
- Entry would be incomprehensible to a future agent without this conversation context
Skip for: simple factual entries, routine appends, O-entry checkpoints.
---
## Anti-Patterns
| Wrong | Right |
|-------|-------|
| L1 too short: "Fixed bug" | Full sentence with root cause + blank line + body |
| Writing English when existing entries are German | Match the store's language |
| Tabs inside content text (e.g. code snippets) | Use spaces for indentation within content — tabs at line start always mean "go deeper in the hierarchy" |
| Mixed spaces and tabs for hierarchy | Stay consistent — either tabs or spaces as your depth marker |
| Everything flat, no indentation | Use hierarchy — L2/L3 for details |
| Save trivial things | Quality over quantity |
| Forget to write_memory | Always call BEFORE setting Status: Completed |
| Write to .hmem via sqlite3/SQL | ONLY use `write_memory` MCP tool — never raw SQL |
| MCP unavailable → skip saving | Reconnect MCP first (`/mcp` or restart tool) |
| `update_memory(id="X", obsolete=true)` without `[✓ID]` | Write correction first, then mark obsolete with `[✓E0076]` tag |