pagelore 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. pagelore-0.4.0/.gitignore +15 -0
  2. pagelore-0.4.0/AGENTS.md +39 -0
  3. pagelore-0.4.0/CHANGELOG.md +601 -0
  4. pagelore-0.4.0/LICENSE +21 -0
  5. pagelore-0.4.0/PKG-INFO +568 -0
  6. pagelore-0.4.0/README.md +542 -0
  7. pagelore-0.4.0/pyproject.toml +80 -0
  8. pagelore-0.4.0/src/pagelore/__init__.py +11 -0
  9. pagelore-0.4.0/src/pagelore/__main__.py +22 -0
  10. pagelore-0.4.0/src/pagelore/cli.py +169 -0
  11. pagelore-0.4.0/src/pagelore/data/AGENT.md +39 -0
  12. pagelore-0.4.0/src/pagelore/data/page-template.md +19 -0
  13. pagelore-0.4.0/src/pagelore/doctor.py +116 -0
  14. pagelore-0.4.0/src/pagelore/index.py +323 -0
  15. pagelore-0.4.0/src/pagelore/init.py +231 -0
  16. pagelore-0.4.0/src/pagelore/instructions.py +158 -0
  17. pagelore-0.4.0/src/pagelore/lib.py +491 -0
  18. pagelore-0.4.0/src/pagelore/search.py +481 -0
  19. pagelore-0.4.0/src/pagelore/stats.py +134 -0
  20. pagelore-0.4.0/src/pagelore/uninstall.py +63 -0
  21. pagelore-0.4.0/src/pagelore/write.py +480 -0
  22. pagelore-0.4.0/tests/conftest.py +65 -0
  23. pagelore-0.4.0/tests/test_concurrency.py +228 -0
  24. pagelore-0.4.0/tests/test_frontmatter.py +29 -0
  25. pagelore-0.4.0/tests/test_hostile_store.py +110 -0
  26. pagelore-0.4.0/tests/test_index.py +267 -0
  27. pagelore-0.4.0/tests/test_init.py +268 -0
  28. pagelore-0.4.0/tests/test_instruction_block.py +166 -0
  29. pagelore-0.4.0/tests/test_log.py +131 -0
  30. pagelore-0.4.0/tests/test_merge.py +152 -0
  31. pagelore-0.4.0/tests/test_read_gate.py +102 -0
  32. pagelore-0.4.0/tests/test_retrieval_quality.py +78 -0
  33. pagelore-0.4.0/tests/test_search.py +38 -0
  34. pagelore-0.4.0/tests/test_stats.py +111 -0
  35. pagelore-0.4.0/tests/test_store_hygiene.py +85 -0
  36. pagelore-0.4.0/tests/test_store_privacy.py +101 -0
  37. pagelore-0.4.0/tests/test_supersede.py +143 -0
  38. pagelore-0.4.0/tests/test_touching.py +138 -0
  39. pagelore-0.4.0/tests/test_version_is_single_sourced.py +126 -0
  40. pagelore-0.4.0/tests/test_write.py +37 -0
  41. pagelore-0.4.0/tests/test_write_validation.py +135 -0
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
5
+ .venv/
6
+ venv/
7
+ .DS_Store
8
+ *.egg-info/
9
+ .modular/
10
+ # Modular: machine-local MCP server path — regenerated per project open
11
+ .mcp.json
12
+
13
+ # Build artefacts
14
+ dist/
15
+ *.egg-info/
@@ -0,0 +1,39 @@
1
+ <!-- pagelore 0.4.0 — the block an agent reads every turn. Managed by `lore`: it is
2
+ rewritten whenever the installed version changes, so edits here are lost. Put your
3
+ own rules in the file that includes this one. Include it by path rather than copying
4
+ it; a copy goes stale on the next release and nothing says so. -->
5
+
6
+ # Project memory
7
+
8
+ Durable decisions, contracts and bug post-mortems live as markdown pages in `.memory/`. Treat them as the record of why this project looks the way it does.
9
+
10
+ **Before stating anything about this project** — what it is, what it does, how a part of it works, why it is that way, what was decided or rejected — and before changing an unfamiliar subsystem, search first:
11
+
12
+ ```bash
13
+ lore search "your query"
14
+ ```
15
+
16
+ The first output line is the store's absolute path; open a full page with `cat <that path>/<slug>.md`. A hit marked `⚠ superseded by <slug>` was replaced — read the replacement first. Add `--touching <path>` to put the pages written against a file you are about to change first. If nothing relevant comes back, say so rather than guessing.
17
+
18
+ The trigger is the kind of claim you are about to make, not the wording of the question; "how does X work" and "what do you know about this project" are memory questions too. This file is not a substitute for the search — it carries instructions rather than reasons, and it goes stale while a page stays dated and sourced. Skip the search only for mechanical work (a command, a typo, a rename) and for general programming questions.
19
+
20
+ **After an architectural decision, a non-obvious bugfix, or a contract change**, write the page:
21
+
22
+ ```bash
23
+ lore write --slug short-kebab-slug --title "One line" --kind decision \
24
+ --source path/to/file --body - <<'PMEOF'
25
+ ## Cause
26
+
27
+ What a future agent could not reconstruct from the code...
28
+ PMEOF
29
+ ```
30
+
31
+ `--kind` is one of `decision`, `bug`, `concept`, `howto`. The terminator is `PMEOF`, not `EOF`, so a page that documents heredocs cannot end its own body early. When a decision reverses an earlier one, add `--supersedes <old-slug>`: that stamps the old page and demotes it, instead of leaving two pages that both read as current.
32
+
33
+ The command validates and rejects: no sources, a source path that does not exist, a resulting page too short to be worth keeping. A rejection exits non-zero and prints a `FIX:` line with the command to run instead — follow it rather than writing the markdown file by hand.
34
+
35
+ Re-running the same slug replaces same-header sections in place and appends new ones, so amendments are cheap and safe.
36
+
37
+ Skip this for typos, reverts, formatting and test-only edits. Before you report the work done, ask whether it changed three or more files; if so, write the page or say in one line that there is nothing worth keeping. One topic per page; cross-link with `[[other-slug]]`.
38
+
39
+ `lore` is on PATH and works from any directory; `lore stats` says what the store has been doing. If the shell answers `lore: command not found`, the program is not on this shell's PATH — say so rather than writing pages by hand, because a page written around the command is skipped by search until someone rewrites it.
@@ -0,0 +1,601 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
+ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.0] - 2026-09-19
10
+
11
+ Read the first two entries before you upgrade. The second one describes a failure
12
+ that produces no error message at all.
13
+
14
+ ### Your notes are safe
15
+
16
+ Nothing about `.memory/` changed — pages, frontmatter, the log and the index are
17
+ byte-compatible, and `lore search` finds every page you already have. There is
18
+ nothing to migrate and nothing to export.
19
+
20
+ ### If you used the `@…/USE.md` include line, your agent will silently load nothing
21
+
22
+ That file lived in the skill directory, and the skill directory is gone. An `@path`
23
+ pointing at a file that does not exist produces **no error in any harness** — the
24
+ agent simply stops searching, and nothing connects that to the upgrade. If your
25
+ `CLAUDE.md` or `GEMINI.md` contains a line like
26
+
27
+ @~/.agents/skills/project-memory/USE.md
28
+
29
+ it is now dead. Fix:
30
+
31
+ ```bash
32
+ sh install.sh --uninstall # takes out the old block; or edit the line out yourself
33
+ pipx install pagelore
34
+ lore init # writes the new block and offers the new line
35
+ ```
36
+
37
+ `lore init` recognises the old marker and replaces that block rather than adding a
38
+ second one, so forgetting the first step costs you nothing. `lore doctor` reports
39
+ an include pointing at a file that is gone.
40
+
41
+ ### If you pasted the block's text instead, you get a loud error
42
+
43
+ Codex and Cursor users pasted the text rather than an import. That copy still tells
44
+ the agent to run a script path that no longer exists, so the agent gets
45
+ `No such file or directory` and can tell you about it. Same fix: `lore init`, then
46
+ replace the pasted block.
47
+
48
+ ### Installing it
49
+
50
+ ```bash
51
+ pipx install pagelore # Python, no Node needed
52
+ npm install -g pagelore # Node, no pip needed — it vendors the Python
53
+ lore init
54
+ ```
55
+
56
+ `lore init` writes the instruction block, then asks which agent should use it and
57
+ shows the exact line and file before changing anything. Its default connects
58
+ nothing. Inside a git repository it also asks whether this project's pages are
59
+ private or tracked.
60
+
61
+ `install.sh` and the `curl … | bash` one-liner still exist, because a published URL
62
+ cannot be recalled and deleting the file would pipe GitHub's 404 page into a shell.
63
+ They install nothing now: they print these commands, still run the old
64
+ `--uninstall` path, and exit 1 so a pipeline fails loudly. Scheduled for deletion
65
+ in 0.6.0.
66
+
67
+ ### Why the name changed
68
+
69
+ The package is `pagelore` and the command is `lore` because `project-memory` and
70
+ `pm` are both already taken on PyPI **and** on npm, by unrelated products in the
71
+ same niche. Not churn — there was no free name to keep.
72
+
73
+ ### Removed: plugin and extension installs
74
+
75
+ The skill directory, all six plugin manifests and the Gemini extension file are
76
+ gone; there is no Agent Skills packaging any more. If you installed it that way,
77
+ that install is separate and this release cannot reach it:
78
+
79
+ ```
80
+ Claude Code /plugin uninstall project-memory
81
+ Gemini CLI gemini extensions uninstall project-memory
82
+ Codex, Cursor, Kimi remove the directory you pointed them at
83
+ ```
84
+
85
+ The marketplace entry is gone too, so `/plugin marketplace add Krowli/project-memory`
86
+ now 404s. Tag `v0.3.5` is the final plugin release and stays installable.
87
+
88
+ **This cost something measurable, and here is the number.** A registered skill
89
+ directory gave a harness a description to index, and that description alone made
90
+ the agent search before answering in **15 of 15** sessions with no instruction line
91
+ anywhere. With nothing registered and no line, it is **0 of 15**. So the packaging
92
+ was not dead weight; it was a working fallback for the user who installs and then
93
+ connects nothing. The recommended setup is 15/15 either way, which is why the trade
94
+ was made, and three things now cover the gap that did not exist before: `lore init`
95
+ runs at install time and offers the line, `lore doctor` calls an unconnected
96
+ install a fault rather than a neutral state, and a bare `lore` says so in one line.
97
+
98
+ ### What you get for it
99
+
100
+ - The command an agent runs is `lore search "…"`, with no path in it. It is correct
101
+ in every layout instead of in one of three. A user hit exactly this in 0.3.4: a
102
+ project-scoped install, and the documented verification command answered
103
+ `No such file or directory`. The instruction block carried three sentences
104
+ explaining which of three layouts you might be in; they are deleted.
105
+ - You can grant an agent `Bash(lore:*)` instead of `Bash(python3:*)` — one program
106
+ rather than arbitrary Python.
107
+ - Updates arrive with `pipx upgrade pagelore`. Twice this month a change landed on
108
+ `main` and reached zero users because no manifest was bumped.
109
+ - The block that makes an agent search is refreshed by every `lore` invocation, so
110
+ a new release does not need you to re-copy anything.
111
+ - `lore doctor` names the three states that used to be invisible: a dangling
112
+ include, a stale pasted copy, a leftover skill directory.
113
+ - `lore uninstall` takes the block back out. `pipx uninstall` cannot, because it
114
+ never learns about a file this program edited.
115
+
116
+ ### Changed, under the hood
117
+
118
+ - One literal in `src/pagelore/__init__.py` is the version. It used to live in ten
119
+ hand-edited places guarded by five drift tests; `npm/package.json` carries the
120
+ only remaining copy, because npm cannot read a Python file, and both the packer
121
+ and a test refuse a mismatch.
122
+ - The built distribution contains the program. Until now it never had: `py-modules
123
+ = []` in `pyproject.toml` silenced an auto-discovery failure, and every published
124
+ artefact held LICENSE, README, pyproject and ten test files. CI now opens the
125
+ wheel and asserts the modules and data files are inside it, installs it with
126
+ pipx on three operating systems, and uses it the way a user does.
127
+ - Seven tests that checked whether one hard-coded script path resolved in three
128
+ documented install layouts are deleted, not replaced. That class of test cannot
129
+ exist any more, which is the clearest single argument for the repackage.
130
+ - The 15 pages in this repository's own store had their `sources:` repointed by
131
+ `tools/retarget_sources.py` rather than through `lore write`, because
132
+ `write_page` unions sources and never removes one — amending through the command
133
+ would have left every page citing both the live path and the dead one, exit 0,
134
+ silently. `updated:` was not bumped: the content did not change that day, and the
135
+ date feeds ranking.
136
+
137
+ ### Added
138
+
139
+ - **`lore init`, `lore doctor`, `lore uninstall`.** Interactivity in `init` is an
140
+ injectable parameter defaulting to `isatty()`, so the questions are tested in
141
+ process with one pty test proving the default wiring. The shell installer this
142
+ replaces shipped a dead prompt — a question placed after the value it decided —
143
+ and it passed review because nothing could reach that branch.
144
+ - **`npm install -g pagelore`**, a shim that finds an interpreter and hands it the
145
+ vendored Python. No pip, no `postinstall`, so `--ignore-scripts` and a corporate
146
+ registry mirror both work. `python -m pagelore` exits 69 below the Python floor
147
+ and the shim reads that as "try the next candidate", printing one message after
148
+ it has tried them all and naming each. Candidate order is platform-specific
149
+ because Windows has no `python3`. `PROJECT_MEMORY_PYTHON` is exclusive: a named
150
+ interpreter that does not work fails loudly instead of falling back silently.
151
+ - **`evals/speed.py`**, so the timing table in the README is reproducible. The
152
+ earlier figures came from an uncommitted script and are superseded rather than
153
+ comparable. One search end to end, 90 pages: 52 ms warm, 76 ms with the index
154
+ refused; 1 000 pages: 69 against 341 ms; 5 000 pages: 139 against 1 507 ms.
155
+ - **`evals/mcp_probe.py`**, a stdio MCP server exposing the two tools, and
156
+ `evals/acceptance.py --pointer mcp|mcp+include` to run a real session against
157
+ it. The CLI was chosen early and never measured against MCP; this settles it
158
+ with numbers rather than opinion. Retrieval quality was never the question —
159
+ the server calls the same functions — so what was measured is whether an agent
160
+ reaches for the memory more reliably when the tools are in its tool list.
161
+ Fifteen sessions per arm: the instruction line 15/15, MCP on Claude Code's
162
+ default settings **0/15**, MCP with `ENABLE_TOOL_SEARCH=false` 15/15, both
163
+ together 15/15. The zero is Claude Code deferring MCP tools behind tool search
164
+ by default, so the single advantage MCP was supposed to have is absent out of
165
+ the box. MCP is not shipped; the probe stays so the question can be re-run.
166
+
167
+ ## [0.3.5] - 2026-09-19
168
+
169
+ The final release installed as a copied skill directory. Nothing in the code
170
+ changed; this tag exists so that the install instructions published for 0.3.x keep
171
+ resolving to something, instead of to a 404, after the packaging is deleted.
172
+
173
+ **The program is now installed with a package manager, and the command is `lore`:**
174
+
175
+ ```bash
176
+ pipx install pagelore # or: npm install -g pagelore
177
+ lore init
178
+ ```
179
+
180
+ Your pages are safe and unchanged — `.memory/` is byte-compatible and `lore search`
181
+ finds every page you already have.
182
+
183
+ If you installed 0.3.x, remove it before or after installing the new one; the two
184
+ do not interfere, but leaving the old block in your agent's instruction file leaves
185
+ an `@path` pointing at a directory that is about to disappear, and a dead `@path`
186
+ produces no error in any harness. The agent just stops searching.
187
+
188
+ ```bash
189
+ sh install.sh --uninstall # the shell install
190
+ /plugin uninstall project-memory # Claude Code
191
+ gemini extensions uninstall project-memory # Gemini CLI
192
+ ```
193
+
194
+ See the 0.4.0 notes for the full upgrade path and for why the name changed:
195
+ `project-memory` and `pm` were both already taken on PyPI and on npm.
196
+
197
+ ## [0.3.4] - 2026-09-19
198
+
199
+ ### Added
200
+
201
+ - **The install asks which agents should use it, and writes the line itself.**
202
+ Printing instructions and leaving the user to carry them out was the step
203
+ people finished the install without taking. It now lists Claude Code, Gemini
204
+ and Codex with the file each one reads, shows the exact path and the exact
205
+ line before touching anything, and writes only after a confirmation. What it
206
+ writes is fenced by marker comments: a second install replaces that block
207
+ rather than stacking another copy, `--uninstall` takes it back out, and
208
+ everything the user wrote around it survives. Declining prints the line to
209
+ add by hand, which is what it used to do unconditionally.
210
+
211
+ ### Fixed
212
+
213
+ - **The "install here or everywhere" question could not change anything.** It
214
+ ran after the destination had already been worked out, so both answers led to
215
+ the same directory. It now runs before, and a pty-driven test asserts that
216
+ answering "only this one" puts the skill in the repository and not in the
217
+ home directory — the questions are interactive, so a test that pipes stdin
218
+ proves nothing about them.
219
+
220
+ ## [0.3.3] - 2026-09-19
221
+
222
+ ### Added
223
+
224
+ - **The install ends by naming the line that makes it work.** It printed where
225
+ the files went and stopped, so someone who ran one command was finished and
226
+ had no way to know they were not: the scripts sit on disk and the agent never
227
+ reaches for them until the instruction block is in its configuration. The
228
+ final message now gives that line with the path filled in for the install it
229
+ just did, the file to put it in for each agent, and the command that undoes
230
+ the whole thing.
231
+ - **It asks whether to install for every project or only for this one**, when
232
+ there is a terminal to answer on and a repository under foot for the second
233
+ option to mean anything. Before, standing inside a project and running it with
234
+ no flags installed globally in silence, which reads as a script with no
235
+ opinion rather than one that made a choice. Piped through CI it stays global
236
+ without stalling.
237
+
238
+ ## [0.3.2] - 2026-09-19
239
+
240
+ ### Added
241
+
242
+ - **`./install.sh --uninstall`.** There was no way to take the skill off a
243
+ machine, so the only instruction anyone could be given was a pair of
244
+ `rm -rf` typed by hand, next to a directory of the user's own pages. It
245
+ removes the skill directory and the symlink the install made, names every
246
+ path it deletes, and leaves three things while saying so: the `.memory/`
247
+ stores in your projects, the line you added to your agent's instruction file,
248
+ and any agent definition you wrote. A symlink is removed only when it
249
+ resolves to the directory being deleted, so a link of yours pointing
250
+ elsewhere survives. It runs before the git and Python checks, because taking
251
+ a program off a machine must not depend on the toolchain that put it there.
252
+ README documents it.
253
+
254
+ ## [0.3.1] - 2026-09-19
255
+
256
+ ### Changed
257
+
258
+ - **README documents how an update actually reaches a user**, per install path,
259
+ because it does not reach them by itself: Claude Code leaves auto-update off
260
+ by default for third-party marketplaces, and a `curl` install has no update
261
+ channel at all beyond `./install.sh --check`. Everything in 0.3.0 that came
262
+ after the tag — the shipped instruction block and the 3.9 floor — was
263
+ unreachable for anyone who had already installed, because a user only
264
+ receives an update when the manifest version changes. Hence this release.
265
+
266
+ - **The Python floor drops from 3.11 to 3.9**, which is what a stock macOS
267
+ already ships — until now `install.sh` refused the interpreter at
268
+ `/usr/bin/python3`, so the skill would not run on a Mac without a Python
269
+ installed first. The floor was never earned: the five scripts carry no 3.10 or
270
+ 3.11 construct, all of them already use `from __future__ import annotations`,
271
+ and the whole suite passes under 3.9.6. CI now runs 3.9 on Linux and Intel
272
+ macOS alongside 3.11 and 3.13 (GitHub publishes no 3.9 build for arm64 macOS
273
+ or Windows), and a test fails if the four places that declare the floor drift
274
+ apart. Windows, which ships no Python at all, stays a documented prerequisite.
275
+ - README rewritten around the three questions people ask first: is the skill
276
+ global or local (global by default, the store is always per project), how to
277
+ make an agent keep the memory without hooks (one paste into the agent's
278
+ global instruction file, paths per vendor docs), and what was measured, with
279
+ every table and the command that reproduces it.
280
+ - The calibration medians quoted in `SKILL.md` and `references/retrieval.md`
281
+ (5.32 against 5.29) predated the FTS5 index; re-measured with the current
282
+ harness they are 9.93 against 8.73. The conclusion is unchanged: a score
283
+ threshold cannot tell an answerable question from an unanswerable one.
284
+
285
+ ## [0.3.0] - 2026-09-16
286
+
287
+ ### Added
288
+
289
+ - **`memory_search.py --touching PATH`** — the pages whose `sources` cite that
290
+ file, or anything under that directory, come first, marked `▸ touches <path>`,
291
+ with or without query words. `sources` was the one field every page must carry
292
+ and the one field ranking never read. Measured on a new 50-query `touching`
293
+ set in `evals/corpus.json`: typing the path as words puts the right page first
294
+ 38% of the time (nDCG@10 0.545); the flag puts it first by construction
295
+ (0.986, paired +0.441 [+0.349, +0.539]). Without the flag nothing changes,
296
+ and the existing tables did not move. Sources are not in the index, so a
297
+ `--touching` search reads every page.
298
+ - **The write gate runs on read.** A page that arrived around `memory_write.py`
299
+ and is under 200 characters is not ranked; search names it on stderr and in
300
+ `--json` so it can be rewritten through the script. A page with no sources is
301
+ shown, marked `⚠ no sources`. `MIN_BODY` moved to `memory_lib` so writer and
302
+ reader share one floor. Search runs on every agent the skill is installed in,
303
+ which is what makes it the place for the check.
304
+ - Every log line carries the session id Claude Code exports to the Bash tool
305
+ (`CLAUDE_CODE_SESSION_ID`), and `memory_stats.py` reports sessions, sessions
306
+ that searched and never wrote, and writes per session — the write side's
307
+ "did it happen", collected by the scripts themselves.
308
+ - `evals/acceptance.py`: a real session of the agent you name, a question only
309
+ the store answers, and a check of the store's log for the search. The only
310
+ proof that an agent uses the memory unprompted; run it per harness.
311
+ - `docs/research/`: primary-source notes behind decisions, starting with how
312
+ superpowers stays portable across thirteen harnesses.
313
+
314
+ ### Removed
315
+
316
+ - **The three Claude Code hooks.** SessionStart announced the memory,
317
+ PreToolUse denied a hand-written page, Stop reminded a session that changed
318
+ files and wrote nothing — and every one existed on one harness while the
319
+ skill claims to work on any. `hooks/` is gone, `install.sh` no longer writes
320
+ `settings.json` (`--no-hook` with it), and the Cursor manifest declares no
321
+ hooks. Announcing the memory is the skill `description`, the context files
322
+ and the `AGENTS.md` snippet, the way superpowers runs on Codex, Devin and
323
+ Grok; the gate moved to read; the reminder became a finishing rule in
324
+ `SKILL.md` and the pointer files. See `.memory/scripts-carry-the-contract-not-hooks.md`.
325
+
326
+ ### Changed
327
+
328
+ - `evals/dense_probe.py --static MODEL` runs the hybrid over a model2vec static
329
+ embedding model and reports the cold process cost next to the shipped search.
330
+ Measured with three `potion` models: no hybrid gain clears zero, and the
331
+ cheapest cold start is 527 ms against 86 ms for the whole shipped search — the
332
+ cheap form of the embedding idea is refused on the same grounds as the
333
+ expensive one. See `references/retrieval.md`.
334
+
335
+ ## [0.2.2] - 2026-08-18
336
+
337
+ ### Fixed
338
+
339
+ - **The hooks were registered as `python3`, which is not a command Windows has.**
340
+ Its installer puts `python`, `py` and `pymanager` on PATH; `python3` exists only
341
+ as an optional versioned alias. So on Windows both hooks silently never ran —
342
+ the agent was never told it had a memory, and the write guard blocked nothing,
343
+ which is precisely the hole it exists to close. `install.sh` now resolves a
344
+ working interpreter (`python3`, then `python`, then `py -3`, each checked for
345
+ 3.11+) and writes that one into `settings.json`; `--interpreter` forces a
346
+ choice. It also no longer accepts a Python below 3.11, which the old existence
347
+ check did — on macOS that is `/usr/bin/python3`, still 3.9.
348
+ - **The suite could not have caught it.** Every hook test invoked `sys.executable`
349
+ and never the string the installer writes, and `actions/setup-python` puts a
350
+ `python3` shim on Windows runners, so both layers hid it. A test now takes the
351
+ command out of the generated `settings.json` and runs it through a shell.
352
+ - `hooks/hooks.json` cannot branch per platform, so the plugin path still
353
+ hard-codes `python3`. That limitation is now in the README rather than in a
354
+ surprise, with a test that keeps the two in sync.
355
+
356
+ ## [0.2.1] - 2026-08-18
357
+
358
+ ### Fixed
359
+
360
+ - **Contended writes terminated a sibling writer on Windows.** The lock decides
361
+ staleness by asking whether the owning process still exists, and used
362
+ `os.kill(pid, 0)` to ask. That is a liveness probe on POSIX and a kill on
363
+ Windows, where every signal but CTRL_C and CTRL_BREAK is delivered by calling
364
+ TerminateProcess. CI caught it as a hung test run, which was the mild version of
365
+ the symptom. Liveness now goes through a platform check — `OpenProcess` on
366
+ Windows, distinguishing "no such process" from "access denied" — and falls back
367
+ to the mtime rule wherever the answer is unknowable. A test pins `os.kill` to
368
+ that one guarded probe.
369
+ - The host identity in a lock file came from `os.uname()`, which does not exist on
370
+ Windows, so every lock there was written and compared as `unknown-host`.
371
+ - **A concurrent search made a write fail outright on Windows.** `os.replace`
372
+ replaces a file regardless of who has it open on POSIX and refuses with
373
+ WinError 5 while any reader holds a handle — and a search reading the store is
374
+ exactly that reader. The replace now retries for a few seconds instead of
375
+ raising.
376
+ - **A process that had exited was reported as running on Windows.** `OpenProcess`
377
+ succeeds for as long as anyone holds a handle to a dead process, so the handle
378
+ alone means nothing; the exit code decides, with 259 (`STILL_ACTIVE`) meaning
379
+ running.
380
+ - **17 of 200 concurrent log lines went missing on Windows.** One `O_APPEND` write
381
+ is atomic against other processes on POSIX and is not on Windows; appends within
382
+ a process are now serialised.
383
+ - Tests that depend on POSIX file modes, `mkfifo` or `geteuid` are skipped on
384
+ Windows rather than faked, so the Windows run reports what it actually covered.
385
+
386
+ ## [0.2.0] - 2026-08-18
387
+
388
+ An audit of the skill against its own claims. Four ways a re-run could destroy
389
+ part of a page, a store that could take retrieval down or leak a file it never
390
+ owned, a write gate with an unguarded side door, and a contract naming a script
391
+ path that existed in one install mode out of four.
392
+
393
+ ### Fixed
394
+
395
+ - **Re-running a write no longer loses content.** `## ` lines inside a code fence
396
+ are content, not headings — the previous splitter deleted the closing fence and
397
+ everything after it. A body with no heading at all used to be dropped whenever
398
+ the page already had lead-in prose. Sections are replaced in place instead of
399
+ moving to the end of the file, so a one-section amendment reads as one in
400
+ `git diff`. Each of these exited 0 and logged a successful write.
401
+ - **Frontmatter fields the tooling does not own are preserved.** Rebuilding from a
402
+ fixed whitelist silently deleted anything else the page carried, which blocked
403
+ extending the format at all.
404
+ - **The 200-character floor applies to the resulting page, not to the increment.**
405
+ Recording "this was reversed in June, here is why" against an existing page was
406
+ refused — the cheapest and most valuable write in the system.
407
+ - **Concurrent writes to one slug no longer lose sections.** An advisory per-page
408
+ lock plus `os.replace`; measured before the fix, twelve to twenty parallel
409
+ writers lost up to 16 of 20 sections, all exiting 0. A reader can no longer
410
+ observe a half-written page, and log lines cannot interleave.
411
+ - **One undecodable page no longer kills every search in the project.** Pages are
412
+ read with `errors="replace"`.
413
+ - **NFD text is findable.** `\w+` does not match combining marks, so the NFD form
414
+ of `ёлка` tokenised as `['е', 'лка']` and recall across an NFC/NFD boundary was
415
+ zero — on macOS, which produces that form. Text is normalised to NFC and folded
416
+ with `casefold()`, which also covers `STRASSE` / `straße`.
417
+ - **A search no longer creates a store or edits `.gitignore`.** Logging a miss used
418
+ to dirty the working tree of a repository that never opted in. The write path
419
+ still creates and shields the store it needs, including on a refusal.
420
+ - **A page symlinked outside the store is ignored.** `ln -s ../.env
421
+ .memory/env-notes.md` made an ordinary search rank and print a secret.
422
+ - **Only top-level `*.md` files are indexed.** `mkdir archive; mv` used to leave the
423
+ page indexed and return two hits with the same slug.
424
+ - **A store that is a dangling symlink is refused with a `FIX:` line** instead of a
425
+ raw `FileExistsError` — the shape `install.sh --store home` produces if its
426
+ target is gone.
427
+ - `memory_stats.py` reports a real median for even counts and tolerates a log line
428
+ from another writer.
429
+ - **Every command in `CLAUDE.md`, `AGENTS.md` and `GEMINI.md` now runs.** All three
430
+ hard-coded `.agents/skills/…`, which exists only after `install.sh --project` —
431
+ not in a clone, which is the case `CLAUDE.md` says it exists for. A test extracts
432
+ every command from those files and checks it.
433
+ - `install.sh --help` no longer truncates mid-table, hiding two store modes.
434
+ - A missing `--kind` is logged as `no_kind` rather than sharing `bad_kind` with an
435
+ invalid one; the two call for opposite fixes.
436
+
437
+ ### Added
438
+
439
+ - **The runtime knows its own version.** `--version` on every script, and the
440
+ session hook tells the agent which version the project is running. A `curl`
441
+ install has no package manager to ask, so until now neither the user nor the
442
+ agent could tell 0.1.0 from 0.2.0 on disk.
443
+ - **`install.sh` installs the latest released tag**, not the tip of `main`, so an
444
+ install is reproducible and a version number means something.
445
+ `PROJECT_MEMORY_REF` still takes a branch or a specific tag, and
446
+ `install.sh --check` reports what is installed against what is released without
447
+ installing anything. A test now guards the `--help` line range, which had
448
+ silently truncated once already.
449
+
450
+ - **A reproducible evaluation, in the repository.** `python3 evals/run.py --by-type`
451
+ over 90 pages and 270 queries with paired bootstrap intervals, plus an ambiguous
452
+ set and an unanswerable set. `evals/gate_value.py` measures what the write gate
453
+ is worth by putting the stubs it refuses back into the corpus. Nothing in
454
+ `references/retrieval.md` is now argued from figures a reader cannot re-run.
455
+ - **A persistent SQLite FTS5 index** (`memory_index.py`), and it is a cache the
456
+ search is allowed to ignore. End to end, as a shell invocation: 235 ms → 99 ms at
457
+ 90 pages, 1887 ms → 174 ms at 1000, 4637 ms → 196 ms at 5000. The ~5000-page
458
+ ceiling the documentation used to name is gone. It lives in the cache directory
459
+ rather than the store, uses no WAL, rebuilds whole rather than repairing, elects
460
+ one builder without waiting, and falls back to reading the markdown on any error
461
+ at all. `PROJECT_MEMORY_NO_FTS5=1` forces the fallback, and CI now runs the whole
462
+ suite twice so that path cannot rot.
463
+ - `--json` reports `served_by`, so two agents served by different paths can explain
464
+ a difference in tail ordering rather than wondering about it.
465
+
466
+ ### Fixed while measuring
467
+
468
+ - **Turkish `İ` was unfindable by its ASCII spelling**: `casefold` turns it into
469
+ `i` plus a combining dot, which matches nothing anyone types.
470
+ - **The evaluation's own FTS5 baseline was misconfigured** — without `tokenchars
471
+ '_'` the pre-tokenised round trip changed 73 of 486 texts — and `bm25()` weights
472
+ are positional over every column, so passing two weights for a three-column table
473
+ gave the title weight to the unindexed slug and left the title at 1.0. The
474
+ title-weight regression test caught the second one.
475
+ - **The supersession tests were still weak.** The fixture is now an unlinked
476
+ control pair: without the link the obsolete page must rank first, and adding the
477
+ link alone must reverse it. The earlier version guarded a score comparison that
478
+ the ranker change quietly invalidated.
479
+
480
+ ### Changed
481
+
482
+ - `references/retrieval.md` reports measured numbers with paired intervals, states
483
+ what the harness cannot tell you, and records the negative result it produced:
484
+ no score or word-overlap threshold can separate a question the store can answer
485
+ from one it cannot (top-hit medians 5.32 against 5.29). `SKILL.md` now tells the
486
+ agent that a result list is not evidence that an answer exists — the previous
487
+ wording, "if search returns nothing relevant, say so", described a case that
488
+ almost never happens.
489
+
490
+ - **`--supersedes <slug>`.** The replaced page is stamped `status: superseded` and
491
+ `superseded_by:`, scored at half its BM25F score and marked in every result
492
+ line. Ranking previously had no recency or authority term and tied
493
+ alphabetically, so a reversed decision could outrank the decision that reversed
494
+ it — the failure the README opens with.
495
+ - **A `PreToolUse` hook that denies a hand-written page** and names
496
+ `memory_write.py` instead. "Writes are refused, not requested" was itself a
497
+ request while the ordinary Write tool could walk around the validator. Escape
498
+ hatch: `PROJECT_MEMORY_ALLOW_HAND_EDIT=1`.
499
+ - **Ranking regression tests.** `W_TITLE` could be set from 5 to 0 — the parameter
500
+ the documentation calls the one that matters — and the whole suite stayed green.
501
+ - Snippets follow the query instead of being the page's first 100 characters, and
502
+ every result line carries the `updated` date; the header line carries the store's
503
+ absolute path, so the documented `cat` works from any directory.
504
+ - `.memory/` in this repository, tracked on purpose. The project had none, across
505
+ fifteen commits of exactly the work it says to record.
506
+ - The session hook fires on `resume` as well, which the test named for it did not
507
+ actually cover.
508
+
509
+ - The documented heredoc terminator is `PMEOF`, not `EOF`: a page documenting
510
+ heredocs ended its own body early and the shell executed the rest of the text.
511
+ - `references/page-format.md` matches the write path — `kind` is required and one
512
+ of four values, `sources` is required, and `note` is gone from both the reference
513
+ and the shipped template.
514
+ - `references/retrieval.md` states that the benchmark's artifacts are not in this
515
+ repository, so its figures are reported rather than reproducible; corrects the
516
+ stub-size units; scopes the latency table to one machine and page size; and
517
+ replaces the claim that `grep` is never cheaper with what it actually is — faster,
518
+ and not an alternative, because it returns an unranked list.
519
+ - README: the injection is ~1.8 KB rather than ~1.3 KB, CI runs on `main` and pull
520
+ requests rather than every push, a user-scope install neither asks about nor
521
+ creates a store, and Contributing names all eight files carrying the version.
522
+ - The read trigger names a class of claim instead of a list of question
523
+ phrasings. Agents read `"why…"` / `"what did we decide…"` as exhaustive and
524
+ answered "what do you know about this project" straight from `AGENTS.md`,
525
+ never searching. It now fires before stating anything about the project —
526
+ what it is, what it does, how a part works, why it is that way.
527
+ - The session hook, `SKILL.md` and the three context files say explicitly that
528
+ `AGENTS.md` / `CLAUDE.md` / `README.md` already in context are not a substitute
529
+ for the search: they carry instructions rather than reasons and they drift,
530
+ while a page stays dated and sourced. Both surfaces also name what does *not*
531
+ need a search — a command, a typo, a rename, a file the user named, general
532
+ programming questions — so the wider trigger does not become a search before
533
+ every turn.
534
+
535
+ ### Fixed after an independent audit of the fixes below
536
+
537
+ The changes above were then audited by agents whose task was to break them. What
538
+ they found, all of it now covered by a test that fails when the fix is removed:
539
+
540
+ - **`--supersedes` copied a symlinked file into the store.** Stamping the replaced
541
+ page walked through its path, so `ln -s ../.env .memory/env-notes.md` — blocked
542
+ on the read path — was read and rewritten as a real page containing the secret,
543
+ which the next search then printed. The write path now refuses to touch anything
544
+ that is not a contained regular file.
545
+ - **A nested ```` fence still exposed a quoted heading.** The fence pattern matched
546
+ exactly three backticks, so the first inner ``` closed a longer outer fence —
547
+ and quoting a memory page, the case the fix was written for, requires exactly
548
+ that. Fences are now three *or more* characters, closed CommonMark-style.
549
+ - **A repeated `## ` header in an incoming body lost its first copy** on a merge,
550
+ because the incoming sections were built as a dict. Introduced by the in-place
551
+ replacement fix. Same-header chunks are joined, and only the first stored
552
+ occurrence is replaced, so a page with two identical headers is no longer
553
+ filled with the same text twice.
554
+ - **An orphan lock defeated the locking.** A lock left by a killed writer was not
555
+ stale for thirty seconds, so every other writer stalled for the full timeout and
556
+ then deleted whatever lock it found — including live ones, losing 2 to 4 of 10
557
+ writers' sections. Staleness is now decided by asking whether the owning process
558
+ still exists, and a live holder's lock is never removed.
559
+ - **One hostile entry could take down every search**: a directory named `notes.md`,
560
+ a broken symlink, an unreadable file — and a FIFO did not fail the search, it
561
+ hung it forever. Only readable regular files are indexed now, and the test for
562
+ it runs in a subprocess with a timeout so a regression fails CI instead of
563
+ hanging it.
564
+ - **The write guard was trivially bypassable**: the extension check was
565
+ case-sensitive, so `.memory/page.MD` was allowed and clobbers `page.md` on a
566
+ case-insensitive filesystem; `page` with no extension was allowed too; and the
567
+ documented `--store home` mode has no `.memory` in its path at all, so it was
568
+ entirely unguarded. It now covers every file in a store, resolves symlinks, and
569
+ stops over-blocking `.memory/../src/x.ts`.
570
+ - **The supersession tests were vacuous.** The whole mechanism could be deleted and
571
+ the suite stayed green, because both fixture pages had the same title and body.
572
+ The fixture now asserts that it is a real inversion before testing the fix.
573
+ - **The session-hook budget was only ever measured from the short in-repo path**; a
574
+ normal install location pushed it to 2201 characters against its own 2000 limit.
575
+ The text is shorter and the prose is bounded separately from the path.
576
+ - Self-supersession and supersession cycles are refused; an empty `--body` is
577
+ refused rather than silently bumping `updated:`; a read-only store produces a
578
+ refusal instead of a traceback; a killed write leaves no `.tmp` behind; a title
579
+ ending in a quote no longer loses that character on every rewrite; and
580
+ `memory_stats.py` reports a true median.
581
+
582
+ ## [0.1.0] - 2026-08-08
583
+
584
+ ### Added
585
+ - `project-memory` skill (`SKILL.md`) with read-before-answer and
586
+ write-after-work workflows.
587
+ - `memory_search.py` — ranked search over the markdown store.
588
+ - `memory_write.py` — create/section-merge pages with stable frontmatter.
589
+ - Claude Code plugin and marketplace manifests.
590
+ - Test suite covering search, writing, frontmatter tolerance and manifests.
591
+
592
+ [Unreleased]: https://github.com/Krowli/project-memory/compare/v0.3.4...HEAD
593
+ [0.3.4]: https://github.com/Krowli/project-memory/compare/v0.3.3...v0.3.4
594
+ [0.3.3]: https://github.com/Krowli/project-memory/compare/v0.3.2...v0.3.3
595
+ [0.3.2]: https://github.com/Krowli/project-memory/compare/v0.3.1...v0.3.2
596
+ [0.3.1]: https://github.com/Krowli/project-memory/compare/v0.3.0...v0.3.1
597
+ [0.3.0]: https://github.com/Krowli/project-memory/compare/v0.2.2...v0.3.0
598
+ [0.2.2]: https://github.com/Krowli/project-memory/compare/v0.2.1...v0.2.2
599
+ [0.2.1]: https://github.com/Krowli/project-memory/compare/v0.2.0...v0.2.1
600
+ [0.2.0]: https://github.com/Krowli/project-memory/compare/v0.1.0...v0.2.0
601
+ [0.1.0]: https://github.com/Krowli/project-memory/releases/tag/v0.1.0