projectmem 0.3.2__tar.gz → 0.3.3__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 (76) hide show
  1. {projectmem-0.3.2 → projectmem-0.3.3}/CHANGELOG.md +40 -0
  2. {projectmem-0.3.2 → projectmem-0.3.3}/PKG-INFO +50 -5
  3. {projectmem-0.3.2 → projectmem-0.3.3}/README.md +49 -4
  4. {projectmem-0.3.2 → projectmem-0.3.3}/pyproject.toml +1 -1
  5. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/__init__.py +1 -1
  6. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/cli.py +11 -1
  7. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/brief.py +2 -1
  8. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/context.py +10 -1
  9. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/dashboard.py +2 -1
  10. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/doctor.py +16 -15
  11. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/global_cmd.py +4 -3
  12. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/hooks.py +38 -8
  13. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/init.py +70 -22
  14. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/precheck.py +20 -6
  15. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/project.py +12 -11
  16. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/score.py +11 -10
  17. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/stats.py +4 -3
  18. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/watch.py +3 -2
  19. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/wrap.py +4 -3
  20. projectmem-0.3.3/src/projectmem/glyphs.py +139 -0
  21. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/mcp_server.py +43 -11
  22. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/staleness.py +80 -10
  23. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/storage.py +15 -0
  24. projectmem-0.3.3/tests/test_agent_guidance.py +280 -0
  25. projectmem-0.3.3/tests/test_staleness_performance.py +146 -0
  26. projectmem-0.3.3/tests/test_windows_console_encoding.py +178 -0
  27. projectmem-0.3.3/tests/test_windows_hooks_and_context.py +204 -0
  28. {projectmem-0.3.2 → projectmem-0.3.3}/.github/FUNDING.yml +0 -0
  29. {projectmem-0.3.2 → projectmem-0.3.3}/.gitignore +0 -0
  30. {projectmem-0.3.2 → projectmem-0.3.3}/LICENSE +0 -0
  31. {projectmem-0.3.2 → projectmem-0.3.3}/SECURITY.md +0 -0
  32. {projectmem-0.3.2 → projectmem-0.3.3}/TUTORIAL.md +0 -0
  33. {projectmem-0.3.2 → projectmem-0.3.3}/llms.txt +0 -0
  34. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/__init__.py +0 -0
  35. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/attempt.py +0 -0
  36. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/auto_capture.py +0 -0
  37. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/backfill.py +0 -0
  38. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/decision.py +0 -0
  39. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/export.py +0 -0
  40. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/fix.py +0 -0
  41. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/instructions.py +0 -0
  42. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/log.py +0 -0
  43. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/map.py +0 -0
  44. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/note.py +0 -0
  45. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/plan.py +0 -0
  46. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/regenerate.py +0 -0
  47. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/search.py +0 -0
  48. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/show.py +0 -0
  49. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/visualize.py +0 -0
  50. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/global_memory.py +0 -0
  51. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/models.py +0 -0
  52. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/project_registry.py +0 -0
  53. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/redaction.py +0 -0
  54. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/resolver.py +0 -0
  55. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/search.py +0 -0
  56. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/structure.py +0 -0
  57. {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/summary.py +0 -0
  58. {projectmem-0.3.2 → projectmem-0.3.3}/tests/conftest.py +0 -0
  59. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_client_config_discovery.py +0 -0
  60. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_dashboard.py +0 -0
  61. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_global_mcp.py +0 -0
  62. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_hooks_path.py +0 -0
  63. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_init.py +0 -0
  64. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_init_ux.py +0 -0
  65. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_location_parsing.py +0 -0
  66. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_log.py +0 -0
  67. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_mcp_server.py +0 -0
  68. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_plan.py +0 -0
  69. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_redaction.py +0 -0
  70. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_search.py +0 -0
  71. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_showoff.py +0 -0
  72. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_structure.py +0 -0
  73. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_summary.py +0 -0
  74. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_v014_features.py +0 -0
  75. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_visualize.py +0 -0
  76. {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_watch.py +0 -0
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.3
4
+
5
+ **Three reports from Windows, and the tool no longer freezes as your project grows.** Every fix below was found by running projectmem on a real machine, not by reading the code.
6
+
7
+ ### Fixed
8
+
9
+ - **`pjm watch --daemon` could not be seen or stopped on Windows, and leaked a process per run.** A hook whose shebang cannot be resolved does not get skipped — git aborts the commit. The hooks shipped `#!/usr/bin/env bash`, which depends on PATH; Git for Windows maps `/bin/sh` to its own shell, and under GitHub Desktop bash often is not there. The snippet body only ever used `[ ]`, `command -v` and `$( )`, so bash bought nothing. Two more faults fed the same failure: the baked binary path went in raw, and on Windows the shell ate its backslashes (`C:\Users\ripon` became `C:\Usersipon`), so the `-x` test failed and the hook silently fell through to its PATH lookup — it only ever worked by accident; and the venv fallback looked in `<prefix>/bin/pjm`, which cannot exist on Windows. ([#16](https://github.com/riponcm/projectmem/issues/16))
10
+
11
+ - **`get_context` described the wrong directory.** `generate_context` defaults its root to `Path.cwd()`, and the MCP server was the only caller that omitted it — both CLI call sites pass it. For an MCP server, cwd is wherever the client launched the process, so the git status and architecture came from the agent harness rather than your project. ([#15](https://github.com/riponcm/projectmem/issues/15))
12
+
13
+ - **A decorative character killed the command on a Windows console.** `pjm init` created `.projectmem/`, the git hooks, `CLAUDE.md` and `AGENTS.md`, then raised `UnicodeEncodeError` printing a box-drawing rule and exited 1 — so the command both did its work and reported failure, which is the worst outcome for any script checking the exit code. A stock Windows install runs the console on cp1252, where `U+2550` has no mapping, and writing it raises rather than degrades. The reported line was only the first one reached: 11 of the 14 glyphs projectmem prints to a terminal are unencodable there. Output streams are now set to replace what they cannot encode, so no glyph can abort a command, and the glyphs themselves fall back to ASCII when the stream cannot carry them — cp1252 output reads as deliberate rather than as a row of `?`. UTF-8 terminals are unchanged, and the web dashboard keeps its real characters.
14
+
15
+ - **Prose punctuation came out as black diamonds on the same console.** A second report against the fix above. An em dash *is* cp1252-encodable (`0x97`), so neither the ASCII fallbacks nor the replacement net touched it — and it still rendered wrong, because Python wrote `0x97` and a terminal set to UTF-8 read that as an invalid lead byte and drew `U+FFFD`. Encodable was never the right test. On a stream that cannot carry Unicode, projectmem now emits no non-ASCII byte at all, so nothing downstream has anything to misread.
16
+
17
+ - **`precheck_file` invented warnings about every project but the server's own.** The tool resolved the project root to read events, then called `_analyze_files` without it, so staleness asked "does this cited file exist?" relative to the MCP server's working directory. In global mode that is the agent harness, so a four-file precheck on an untouched repo came back with 373 phantom "cited file no longer exists" warnings. `_analyze_files` had the same gap one level down — it took the root but called `_git_recent_changes` without it, so `git log` ran in the wrong repository, and outside a repository git fails and the churn count silently fell back to the event log, reporting churn the project did not have. Both surface only when the root differs from cwd, which is every MCP client that does not chdir into the project and never the CLI — which is why `pjm precheck` was always correct.
18
+
19
+ - **Expected errors were reported as crashes.** `pjm fix` with no open issue printed a clear message and then a full traceback underneath it. `typer.Exit` escaped `main()` uncaught, so Python printed the chained cause.
20
+
21
+ ### Faster
22
+
23
+ - **`pjm precheck` was taking 26 seconds on a 1,200-event project, and getting worse as the project aged.** It ran one `git log` per event — 1,201 subprocesses — to answer a question about a single file, and the instructions tell agents to call it before modifying anything. Three faults compounded: staleness was computed across the whole event log and then filtered down to the file being checked; the memo key was `(file, timestamp)`, which looks right but never hits because real events carry distinct timestamps; and nothing bounded the git walk. It now makes one `git log` per distinct file, bounded by the oldest event citing it, and counts in memory.
24
+
25
+ | events | before | after |
26
+ |---|---|---|
27
+ | 100 | 2,210 ms | 48 ms |
28
+ | 400 | 8,905 ms | 51 ms |
29
+ | 1,500 | ~33 s, 1,501 git processes | **82 ms, 2 processes** |
30
+
31
+ Latency is now flat rather than linear in project age. The project-wide path used by `pjm brief` and the dashboard went from 26,294 ms to 450 ms. Results are unchanged — verified against a reimplementation of the old algorithm, including across merge commits.
32
+
33
+ ### Changed
34
+
35
+ - **Your project is named in `CLAUDE.md`.** One server serves every project, so a tool call that names none is refused rather than guessed at — but nothing ever told the agent the name, so it learned it from the error and retried. That round trip happened every session. The bridge now carries the registered name and the exact argument to pass. ([#14](https://github.com/riponcm/projectmem/issues/14))
36
+
37
+ - **`AGENTS.md` is written alongside `CLAUDE.md`.** The 0.3.3 fix for #14 put the project name in `CLAUDE.md` — which Antigravity and Codex never read, so for those clients the fix did not exist. `pjm init` now writes both files with identical content, and the guidance no longer asks the agent to look the project up before starting: it names the active project first and says calls that omit `project` land there. ([#14](https://github.com/riponcm/projectmem/issues/14))
38
+
39
+ - **Retiring a decision is discoverable.** `supersedes` has existed since 0.1.4 and the summary renderer honours it, but `AI_INSTRUCTIONS.md` mentioned it zero times in 12,503 characters, so models never called it and `summary.md` accumulated decisions that contradicted each other. It is documented now on all three surfaces an agent may read, and each says what it does to the summary rather than only that the argument exists. ([#17](https://github.com/riponcm/projectmem/issues/17))
40
+
41
+ Windows daemon support in 0.3.2 was contributed by [@medium-effort](https://github.com/medium-effort), who also reported every issue fixed in this release.
42
+
3
43
  ## 0.3.2
4
44
 
5
45
  **Windows works properly now.** `pjm watch --daemon` crashed there, and fixing the crash uncovered a second bug one function away that had been hiding behind it. Both are fixed. `pjm doctor` also learned to notice when a fix you made gets undone.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: projectmem
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Coding agent memory — local-first memory and judgment for AI coding agents. One MCP server for every project; warns before repeating failed fixes.
5
5
  Project-URL: Homepage, https://projectmem.dev
6
6
  Project-URL: Repository, https://github.com/riponcm/projectmem
@@ -35,8 +35,8 @@ Description-Content-Type: text/markdown
35
35
  <img src="https://raw.githubusercontent.com/projectmem/projectmemdoc/main/logo/projectmem-wordmark-800.png" alt="projectmem" width="420" />
36
36
 
37
37
  <p>
38
- <a href="https://github.com/riponcm/projectmem/blob/main/CHANGELOG.md"><b>🎉 v0.3.2 is out</b></a> —
39
- Windows support for the file watcher, and a doctor that notices when a fix gets undone.
38
+ <a href="https://github.com/riponcm/projectmem/blob/main/CHANGELOG.md"><b>🎉 v0.3.3 is out</b></a> —
39
+ `pjm precheck` no longer slows down as your project grows, and Windows works end to end.
40
40
  <a href="https://github.com/riponcm/projectmem/blob/main/CHANGELOG.md">See what changed →</a>
41
41
  </p>
42
42
 
@@ -195,7 +195,7 @@ cd your-project && pjm init
195
195
  |---|---|
196
196
  | **[Complete setup guide](https://projectmem.dev/blog/projectmem-complete-guide-ai-coding-agent-memory)** | The full walkthrough on the web — install, MCP setup per client, `pjm doctor`, your first logged issue, and both dashboards. Every terminal output is captured from a real run. |
197
197
  | **[TUTORIAL.md](TUTORIAL.md)** | 15-minute step-by-step walkthrough — set up projectmem on your own project, watch the lifecycle, see the pre-commit warning fire. |
198
- | **[CHANGELOG.md](CHANGELOG.md)** | Release history. Latest: v0.3.2 — the file watcher works on Windows, and `pjm doctor` spots a config fix that got reverted. |
198
+ | **[CHANGELOG.md](CHANGELOG.md)** | Release history. Latest: v0.3.3 — `pjm precheck` is flat rather than linear in project age, and six Windows reports are closed. |
199
199
  | **[Research paper (arXiv:2606.12329)](https://arxiv.org/abs/2606.12329)** | *PROJECTMEM: A Local-First, Event-Sourced Memory and Judgment Layer for AI Coding Agents* — the peer-readable version: design, Memory-as-Governance framing, capability comparison, and the 207-event dogfooding study. |
200
200
  | **[LICENSE](LICENSE)** | MIT |
201
201
 
@@ -232,6 +232,51 @@ That's it. `pjm init` installs three git hooks (pre-commit warnings, post-commit
232
232
 
233
233
  ---
234
234
 
235
+ ## ✨ New in 0.3.3 — precheck stops getting slower, and Windows works end to end
236
+
237
+ `pjm precheck` runs before every edit — the instructions tell agents to call it
238
+ first — so its cost is paid constantly. It was taking **26 seconds on a
239
+ 1,200-event project**, and getting worse every week, because it ran one
240
+ `git log` per event to answer a question about a single file. It now makes one
241
+ call per distinct *file*, bounded by the oldest event citing it.
242
+
243
+ | events | before | after |
244
+ |---|---|---|
245
+ | 100 | 2,210 ms | 48 ms |
246
+ | 400 | 8,905 ms | 51 ms |
247
+ | 1,500 | ~33 s, 1,501 git processes | **82 ms, 2 processes** |
248
+
249
+ Latency is now flat rather than linear in project age. Results are unchanged —
250
+ verified against a reimplementation of the old algorithm, including across
251
+ merge commits.
252
+
253
+ **A retired decision no longer resurfaces.** `supersedes` has existed since
254
+ 0.1.4, but the two surfaces an agent actually reads during work — `get_context`
255
+ and `precheck_file` — were not filtering it. You could retire a decision and
256
+ still be told about it. Both filter now, and `AI_INSTRUCTIONS.md` finally
257
+ documents how to retire one, which is why models never called it.
258
+
259
+ **Your project is named in the bridge file, and `AGENTS.md` is written too.**
260
+ One server serves every project, so a call naming none is refused rather than
261
+ guessed at — but nothing told the agent the name, so it learned it from the
262
+ error and retried, every session. The name is in the bridge now. It goes into
263
+ `AGENTS.md` as well as `CLAUDE.md`, because Antigravity and Codex never read
264
+ the latter.
265
+
266
+ **Six Windows reports are closed.** Git hooks shipped a `bash` shebang that
267
+ Git for Windows often cannot resolve — and an unresolvable shebang does not
268
+ skip the hook, it aborts your commit. The baked binary path lost its
269
+ backslashes to shell escaping. The venv fallback looked in `bin/`, which
270
+ cannot exist there. `pjm watch --daemon` could not be seen or stopped. And a
271
+ box-drawing character in the output killed `pjm init` outright on a cp1252
272
+ console — after it had already created everything, so the command both did its
273
+ work and reported failure.
274
+
275
+ Every one was fixed by running projectmem on the machines that reported it —
276
+ Windows 11, and the four MCP clients — not by reading the code. Reported by
277
+ [@medium-effort](https://github.com/riponcm/projectmem/issues), who also
278
+ contributed the 0.3.2 Windows daemon support.
279
+
235
280
  ## ✨ New in 0.3.2 — Windows, properly
236
281
 
237
282
  `pjm watch --daemon` crashed on Windows with `AttributeError: module 'os' has no
@@ -388,7 +433,7 @@ Your project's memory is also something you can *watch* — and share.
388
433
 
389
434
  ## 🚧 Upcoming
390
435
 
391
- - **Import your existing memory** — `pjm import` *(planned for 0.3.3)* will migrate history from **mem0**, **agentmemory**, **Letta**, and Claude session logs into projectmem. It maps only to the core event vocabulary — issues, attempts, fixes, decisions, notes — so signal comes in and another tool's clutter stays out. Your judgment history moves with you.
436
+ - **Import your existing memory** — `pjm import` *(planned for 0.4.0)* will migrate history from **mem0**, **agentmemory**, **Letta**, and Claude session logs into projectmem. It maps only to the core event vocabulary — issues, attempts, fixes, decisions, notes — so signal comes in and another tool's clutter stays out. Your judgment history moves with you.
392
437
 
393
438
  Want a source supported? [Open an issue](https://github.com/riponcm/projectmem/issues) and tell us what you're migrating from.
394
439
 
@@ -4,8 +4,8 @@
4
4
  <img src="https://raw.githubusercontent.com/projectmem/projectmemdoc/main/logo/projectmem-wordmark-800.png" alt="projectmem" width="420" />
5
5
 
6
6
  <p>
7
- <a href="https://github.com/riponcm/projectmem/blob/main/CHANGELOG.md"><b>🎉 v0.3.2 is out</b></a> —
8
- Windows support for the file watcher, and a doctor that notices when a fix gets undone.
7
+ <a href="https://github.com/riponcm/projectmem/blob/main/CHANGELOG.md"><b>🎉 v0.3.3 is out</b></a> —
8
+ `pjm precheck` no longer slows down as your project grows, and Windows works end to end.
9
9
  <a href="https://github.com/riponcm/projectmem/blob/main/CHANGELOG.md">See what changed →</a>
10
10
  </p>
11
11
 
@@ -164,7 +164,7 @@ cd your-project && pjm init
164
164
  |---|---|
165
165
  | **[Complete setup guide](https://projectmem.dev/blog/projectmem-complete-guide-ai-coding-agent-memory)** | The full walkthrough on the web — install, MCP setup per client, `pjm doctor`, your first logged issue, and both dashboards. Every terminal output is captured from a real run. |
166
166
  | **[TUTORIAL.md](TUTORIAL.md)** | 15-minute step-by-step walkthrough — set up projectmem on your own project, watch the lifecycle, see the pre-commit warning fire. |
167
- | **[CHANGELOG.md](CHANGELOG.md)** | Release history. Latest: v0.3.2 — the file watcher works on Windows, and `pjm doctor` spots a config fix that got reverted. |
167
+ | **[CHANGELOG.md](CHANGELOG.md)** | Release history. Latest: v0.3.3 — `pjm precheck` is flat rather than linear in project age, and six Windows reports are closed. |
168
168
  | **[Research paper (arXiv:2606.12329)](https://arxiv.org/abs/2606.12329)** | *PROJECTMEM: A Local-First, Event-Sourced Memory and Judgment Layer for AI Coding Agents* — the peer-readable version: design, Memory-as-Governance framing, capability comparison, and the 207-event dogfooding study. |
169
169
  | **[LICENSE](LICENSE)** | MIT |
170
170
 
@@ -201,6 +201,51 @@ That's it. `pjm init` installs three git hooks (pre-commit warnings, post-commit
201
201
 
202
202
  ---
203
203
 
204
+ ## ✨ New in 0.3.3 — precheck stops getting slower, and Windows works end to end
205
+
206
+ `pjm precheck` runs before every edit — the instructions tell agents to call it
207
+ first — so its cost is paid constantly. It was taking **26 seconds on a
208
+ 1,200-event project**, and getting worse every week, because it ran one
209
+ `git log` per event to answer a question about a single file. It now makes one
210
+ call per distinct *file*, bounded by the oldest event citing it.
211
+
212
+ | events | before | after |
213
+ |---|---|---|
214
+ | 100 | 2,210 ms | 48 ms |
215
+ | 400 | 8,905 ms | 51 ms |
216
+ | 1,500 | ~33 s, 1,501 git processes | **82 ms, 2 processes** |
217
+
218
+ Latency is now flat rather than linear in project age. Results are unchanged —
219
+ verified against a reimplementation of the old algorithm, including across
220
+ merge commits.
221
+
222
+ **A retired decision no longer resurfaces.** `supersedes` has existed since
223
+ 0.1.4, but the two surfaces an agent actually reads during work — `get_context`
224
+ and `precheck_file` — were not filtering it. You could retire a decision and
225
+ still be told about it. Both filter now, and `AI_INSTRUCTIONS.md` finally
226
+ documents how to retire one, which is why models never called it.
227
+
228
+ **Your project is named in the bridge file, and `AGENTS.md` is written too.**
229
+ One server serves every project, so a call naming none is refused rather than
230
+ guessed at — but nothing told the agent the name, so it learned it from the
231
+ error and retried, every session. The name is in the bridge now. It goes into
232
+ `AGENTS.md` as well as `CLAUDE.md`, because Antigravity and Codex never read
233
+ the latter.
234
+
235
+ **Six Windows reports are closed.** Git hooks shipped a `bash` shebang that
236
+ Git for Windows often cannot resolve — and an unresolvable shebang does not
237
+ skip the hook, it aborts your commit. The baked binary path lost its
238
+ backslashes to shell escaping. The venv fallback looked in `bin/`, which
239
+ cannot exist there. `pjm watch --daemon` could not be seen or stopped. And a
240
+ box-drawing character in the output killed `pjm init` outright on a cp1252
241
+ console — after it had already created everything, so the command both did its
242
+ work and reported failure.
243
+
244
+ Every one was fixed by running projectmem on the machines that reported it —
245
+ Windows 11, and the four MCP clients — not by reading the code. Reported by
246
+ [@medium-effort](https://github.com/riponcm/projectmem/issues), who also
247
+ contributed the 0.3.2 Windows daemon support.
248
+
204
249
  ## ✨ New in 0.3.2 — Windows, properly
205
250
 
206
251
  `pjm watch --daemon` crashed on Windows with `AttributeError: module 'os' has no
@@ -357,7 +402,7 @@ Your project's memory is also something you can *watch* — and share.
357
402
 
358
403
  ## 🚧 Upcoming
359
404
 
360
- - **Import your existing memory** — `pjm import` *(planned for 0.3.3)* will migrate history from **mem0**, **agentmemory**, **Letta**, and Claude session logs into projectmem. It maps only to the core event vocabulary — issues, attempts, fixes, decisions, notes — so signal comes in and another tool's clutter stays out. Your judgment history moves with you.
405
+ - **Import your existing memory** — `pjm import` *(planned for 0.4.0)* will migrate history from **mem0**, **agentmemory**, **Letta**, and Claude session logs into projectmem. It maps only to the core event vocabulary — issues, attempts, fixes, decisions, notes — so signal comes in and another tool's clutter stays out. Your judgment history moves with you.
361
406
 
362
407
  Want a source supported? [Open an issue](https://github.com/riponcm/projectmem/issues) and tell us what you're migrating from.
363
408
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "projectmem"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Coding agent memory — local-first memory and judgment for AI coding agents. One MCP server for every project; warns before repeating failed fixes."
9
9
  authors = [{ name = "Ripon Chandra Malo" }]
10
10
  readme = "README.md"
@@ -1,3 +1,3 @@
1
1
  """Local-first memory for repos."""
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.3.3"
@@ -4,6 +4,7 @@ from pathlib import Path
4
4
 
5
5
  import typer
6
6
 
7
+ from projectmem.glyphs import configure_stdio
7
8
  from projectmem.commands import attempt as attempt_command
8
9
  from projectmem.commands import auto_capture as auto_capture_command
9
10
  from projectmem.commands import backfill as backfill_command
@@ -451,12 +452,21 @@ def _upgrade_notice() -> None:
451
452
 
452
453
 
453
454
  def main() -> None:
455
+ # Before anything prints. A legacy Windows console runs a code page that
456
+ # cannot encode our box-drawing glyphs, and an unencodable character does
457
+ # not degrade — it raises, mid-command, after the side effects have
458
+ # already landed (#18).
459
+ configure_stdio()
454
460
  _upgrade_notice()
455
461
  try:
456
462
  app()
457
463
  except ProjectMemError as exc:
458
464
  typer.echo(f"Error: {exc}", err=True)
459
- raise typer.Exit(1) from exc
465
+ # SystemExit, not typer.Exit: nothing catches typer.Exit here, so it
466
+ # escaped main() and Python printed the whole chained traceback under
467
+ # the message. An expected condition — "no open issue yet" — was being
468
+ # reported as a crash.
469
+ raise SystemExit(1) from None
460
470
 
461
471
 
462
472
  if __name__ == "__main__":
@@ -15,6 +15,7 @@ from pathlib import Path
15
15
 
16
16
  import typer
17
17
 
18
+ from projectmem.glyphs import RULE
18
19
  from projectmem.models import Event, location_to_file, superseded_ids
19
20
  from projectmem.storage import read_events
20
21
 
@@ -55,7 +56,7 @@ def _safe_echo(text: object = "") -> None:
55
56
  def _rule(width: int = 60) -> str:
56
57
  encoding = _stdout_encoding().lower()
57
58
  if "utf" in encoding:
58
- return "─" * width
59
+ return RULE * width
59
60
  return "-" * width
60
61
 
61
62
 
@@ -22,7 +22,7 @@ from typing import Any
22
22
 
23
23
  import typer
24
24
 
25
- from projectmem.models import Event, location_to_file
25
+ from projectmem.models import Event, location_to_file, superseded_ids
26
26
  from projectmem.storage import (
27
27
  read_events,
28
28
  require_mem_dir,
@@ -108,8 +108,17 @@ def generate_context(
108
108
  git_files = _get_git_status_files(root_path)
109
109
 
110
110
  # ── Score and filter events ──
111
+ # Retired decisions are dropped. summary.md has always filtered these, but
112
+ # this block did not — so a model could correctly call add_decision(...,
113
+ # supersedes=...), see summary.md show one decision, and still be handed
114
+ # both here with equal timestamps and no way to tell which is current.
115
+ # That is the exact failure superseding exists to prevent, on the surface
116
+ # agents actually read. The log keeps both; only the live view filters.
117
+ retired = superseded_ids(events)
111
118
  scored: list[tuple[float, Event]] = []
112
119
  for event in events:
120
+ if event.id in retired:
121
+ continue
113
122
  score = _score_event(event, now, cutoff, focus, git_files)
114
123
  if score > 0:
115
124
  scored.append((score, event))
@@ -20,6 +20,7 @@ from pathlib import Path
20
20
  import typer
21
21
 
22
22
  from projectmem import __version__
23
+ from projectmem.glyphs import ARROW, RUNNING
23
24
  from projectmem.models import Event
24
25
  from projectmem.storage import read_events, registered_projects, registry_path
25
26
  from projectmem.commands.score import calculate_score
@@ -412,7 +413,7 @@ def _serve(port: int, open_browser: bool) -> None:
412
413
 
413
414
  url = f"http://127.0.0.1:{port}"
414
415
  typer.echo("")
415
- typer.echo(f" ● Live global dashboard → {url}")
416
+ typer.echo(f" {RUNNING} Live global dashboard {ARROW} {url}")
416
417
  typer.echo(" Reads every project's files fresh on each load; the Refresh")
417
418
  typer.echo(" button pulls the latest. Ephemeral — no background daemon.")
418
419
  typer.echo(" Press Ctrl+C to stop.")
@@ -22,6 +22,7 @@ from pathlib import Path
22
22
  import typer
23
23
 
24
24
  from projectmem import __version__
25
+ from projectmem.glyphs import FAIL, OK, WARN
25
26
  from projectmem.commands.init import (
26
27
  _pinned_client_configs,
27
28
  _projectmem_client_configs,
@@ -189,7 +190,7 @@ def _update_line(online: bool) -> None:
189
190
  meta = load_meta()
190
191
  enabled = bool(meta.get("update_check"))
191
192
  if not (online or enabled):
192
- typer.secho(f"\n ✓ Running {__version__}", fg=typer.colors.GREEN)
193
+ typer.secho(f"\n {OK} Running {__version__}", fg=typer.colors.GREEN)
193
194
  typer.echo(" Check PyPI for a newer release: pjm doctor --online")
194
195
  typer.echo(" Check daily from now on: pjm doctor --auto")
195
196
  typer.echo(" (projectmem makes no network calls unless you ask.)")
@@ -207,17 +208,17 @@ def _update_line(online: bool) -> None:
207
208
  meta["update_latest"] = fetched
208
209
  save_meta(meta)
209
210
  if not newest:
210
- typer.secho(f"\n ✓ Running {__version__}", fg=typer.colors.GREEN)
211
+ typer.secho(f"\n {OK} Running {__version__}", fg=typer.colors.GREEN)
211
212
  typer.echo(" Could not reach PyPI — nothing else was affected.")
212
213
  return
213
214
  if _as_tuple(newest) > _as_tuple(__version__):
214
215
  typer.secho(
215
- f"\n ⚠ {newest} is available (you have {__version__})",
216
+ f"\n {WARN} {newest} is available (you have {__version__})",
216
217
  fg=typer.colors.YELLOW,
217
218
  )
218
219
  typer.echo(" pip install -U projectmem")
219
220
  else:
220
- typer.secho(f"\n ✓ Running {__version__} — the latest", fg=typer.colors.GREEN)
221
+ typer.secho(f"\n {OK} Running {__version__} — the latest", fg=typer.colors.GREEN)
221
222
 
222
223
 
223
224
 
@@ -299,7 +300,7 @@ def run(
299
300
  if auto is not None:
300
301
  set_auto_check(auto)
301
302
  typer.secho(
302
- f"✓ Automatic update checks {'on (once a day)' if auto else 'off'}\n",
303
+ f"{OK} Automatic update checks {'on (once a day)' if auto else 'off'}\n",
303
304
  fg=typer.colors.GREEN,
304
305
  )
305
306
  scan_roots = [r.expanduser().resolve() for r in roots] if roots else default_roots()
@@ -333,7 +334,7 @@ def run(
333
334
  if missing:
334
335
  problems += 1
335
336
  typer.secho(
336
- f" ⚠ {len(missing)} project(s) have memory but are not registered",
337
+ f" {WARN} {len(missing)} project(s) have memory but are not registered",
337
338
  fg=typer.colors.YELLOW,
338
339
  )
339
340
  for project in missing[:10]:
@@ -345,24 +346,24 @@ def run(
345
346
  try:
346
347
  register(project)
347
348
  except RegistryError as exc:
348
- typer.secho(f" ✗ {project}: {exc}", fg=typer.colors.RED)
349
- typer.secho(f" ✓ Registered {len(missing)}", fg=typer.colors.GREEN)
349
+ typer.secho(f" {FAIL} {project}: {exc}", fg=typer.colors.RED)
350
+ typer.secho(f" {OK} Registered {len(missing)}", fg=typer.colors.GREEN)
350
351
  else:
351
- typer.secho(f" ✓ All {len(found)} project(s) found are registered", fg=typer.colors.GREEN)
352
+ typer.secho(f" {OK} All {len(found)} project(s) found are registered", fg=typer.colors.GREEN)
352
353
 
353
354
  # ── 2. registry entries whose memory is gone ──
354
355
  stale = [r for r in registry.projects if not (r.path / MEM_DIR).is_dir()]
355
356
  if stale:
356
357
  problems += 1
357
- typer.secho(f"\n ⚠ {len(stale)} registered project(s) no longer have memory", fg=typer.colors.YELLOW)
358
+ typer.secho(f"\n {WARN} {len(stale)} registered project(s) no longer have memory", fg=typer.colors.YELLOW)
358
359
  for record in stale:
359
360
  typer.echo(f" {record.name} {record.path}")
360
361
  if fix:
361
362
  for record in stale:
362
363
  unregister(record.id)
363
- typer.secho(f" ✓ Removed {len(stale)} from the registry (repos untouched)", fg=typer.colors.GREEN)
364
+ typer.secho(f" {OK} Removed {len(stale)} from the registry (repos untouched)", fg=typer.colors.GREEN)
364
365
  else:
365
- typer.secho("\n ✓ Every registered project still has its memory", fg=typer.colors.GREEN)
366
+ typer.secho(f"\n {OK} Every registered project still has its memory", fg=typer.colors.GREEN)
366
367
 
367
368
  # ── 3. client configs still pinned to one repo ──
368
369
  # Scanned once here so the project-scoped configs are included and the
@@ -372,14 +373,14 @@ def run(
372
373
  pinned = [(c, p) for c, p, st in scanned if st == "pinned"]
373
374
  if unreadable:
374
375
  problems += 1
375
- typer.secho("\n ⚠ Could not read some client config(s) — not checked",
376
+ typer.secho(f"\n {WARN} Could not read some client config(s) — not checked",
376
377
  fg=typer.colors.YELLOW)
377
378
  for client, path in unreadable:
378
379
  typer.echo(f" {client} {path}")
379
380
  typer.echo(" A config that cannot be read is not a config that is fine.")
380
381
  if pinned:
381
382
  problems += 1
382
- typer.secho("\n ⚠ MCP client config(s) still pinned to a single repo", fg=typer.colors.YELLOW)
383
+ typer.secho(f"\n {WARN} MCP client config(s) still pinned to a single repo", fg=typer.colors.YELLOW)
383
384
  for client, path in pinned:
384
385
  typer.echo(f" {client} {path}")
385
386
  typer.echo(' Remove --root / cwd / PROJECTMEM_ROOT from the projectmem entry.')
@@ -396,7 +397,7 @@ def run(
396
397
  fg=typer.colors.RED)
397
398
  else:
398
399
  _reverted_configs([]) # record the clean state so a later revert is visible
399
- typer.secho("\n ✓ No MCP client config is pinned to a single repo", fg=typer.colors.GREEN)
400
+ typer.secho(f"\n {OK} No MCP client config is pinned to a single repo", fg=typer.colors.GREEN)
400
401
 
401
402
  _update_line(online)
402
403
 
@@ -20,6 +20,7 @@ from pathlib import Path
20
20
 
21
21
  import typer
22
22
 
23
+ from projectmem.glyphs import RULE
23
24
  from projectmem.global_memory import (
24
25
  add_gotcha,
25
26
  add_pattern,
@@ -167,7 +168,7 @@ def _list_entries(
167
168
 
168
169
  if patterns:
169
170
  typer.echo(f"\n{bold}Patterns ({len(patterns)}){reset}")
170
- typer.echo(f"{dim}{'─' * 60}{reset}")
171
+ typer.echo(f"{dim}{f'{RULE}' * 60}{reset}")
171
172
  for p in patterns:
172
173
  pid = p.get("id", "?")
173
174
  text = p.get("pattern", "")
@@ -187,7 +188,7 @@ def _list_entries(
187
188
 
188
189
  if gotchas:
189
190
  typer.echo(f"\n{bold}Library Gotchas ({len(gotchas)}){reset}")
190
- typer.echo(f"{dim}{'─' * 60}{reset}")
191
+ typer.echo(f"{dim}{f'{RULE}' * 60}{reset}")
191
192
  for g in gotchas:
192
193
  gid = g.get("id", "?")
193
194
  lib = g.get("library", "unknown")
@@ -302,7 +303,7 @@ def _detect(root: Path | None = None, fmt: str = "text") -> None:
302
303
  reset = "\033[0m"
303
304
 
304
305
  typer.echo(f"\n{bold}Detected Stack: {root_path.name}{reset}")
305
- typer.echo(f"{dim}{'─' * 50}{reset}")
306
+ typer.echo(f"{dim}{f'{RULE}' * 50}{reset}")
306
307
 
307
308
  if stack["tags"]:
308
309
  typer.echo(f" Tags: {cyan}{', '.join(stack['tags'])}{reset}")
@@ -15,6 +15,13 @@ from projectmem.storage import MEM_DIR
15
15
  HOOK_MARKER_START = "# >>> projectmem auto-capture >>>"
16
16
  HOOK_MARKER_END = "# <<< projectmem auto-capture <<<"
17
17
 
18
+ # /bin/sh, not bash. Git for Windows maps /bin/sh to its bundled shell, while
19
+ # `/usr/bin/env bash` depends on what is on PATH — and under GitHub Desktop it
20
+ # often is not there, which makes git abort the commit outright rather than
21
+ # skip the hook. The snippet body uses only [ ], command -v and $( ), so there
22
+ # is nothing here that needs bash.
23
+ HOOK_SHEBANG = "#!/bin/sh\n"
24
+
18
25
 
19
26
  # ── L-047: bake the absolute path to `pjm` into the hook ────────────────
20
27
  #
@@ -26,23 +33,46 @@ HOOK_MARKER_END = "# <<< projectmem auto-capture <<<"
26
33
  # fallback to `command -v` for the rare case where the install-time binary
27
34
  # was moved.
28
35
 
36
+ def _shell_path(path: str) -> str:
37
+ """A path safe to embed in a POSIX shell script.
38
+
39
+ Git hooks run under a shell even on Windows — Git for Windows carries its
40
+ own POSIX runtime. A native Windows path goes in as
41
+ ``C:\\Users\\ripon\\...`` and the shell reads every backslash as an
42
+ escape, so ``PJM_BIN`` becomes ``C:\\Usersipon\\...``: the ``-x`` test
43
+ then fails and the hook silently falls through to its PATH lookup. Forward
44
+ slashes are understood by Git's shell and by Windows itself.
45
+ """
46
+ return path.replace("\\", "/")
47
+
48
+
29
49
  def _resolve_pjm_binary() -> str:
30
50
  """Find an absolute path to a working pjm-equivalent CLI.
31
51
 
32
52
  Preference order:
33
53
  1. ``pjm`` on PATH (most common — pip-installed entry point)
34
54
  2. ``projectmem`` on PATH (alias entry point)
35
- 3. ``<sys.prefix>/bin/pjm`` (conda / venv layout where the entry
36
- point lives next to the python that imported this module)
55
+ 3. The entry point next to the interpreter that imported this module —
56
+ ``<prefix>/bin/pjm`` on POSIX, ``<prefix>/Scripts/pjm.exe`` on
57
+ Windows. Looking only in ``bin/`` meant this branch could never
58
+ match on Windows.
37
59
  4. Bare ``"pjm"`` as a last resort — preserves prior behaviour and
38
60
  the runtime fallback in the snippet can still find it.
61
+
62
+ The result is always shell-safe: see ``_shell_path``.
39
63
  """
40
64
  found = shutil.which("pjm") or shutil.which("projectmem")
41
65
  if found:
42
- return found
43
- venv_guess = Path(sys.prefix) / "bin" / "pjm"
44
- if venv_guess.exists():
45
- return str(venv_guess)
66
+ return _shell_path(found)
67
+ if os.name == "nt":
68
+ candidates = [Path(sys.prefix) / "Scripts" / "pjm.exe",
69
+ Path(sys.prefix) / "Scripts" / "projectmem.exe"]
70
+ else:
71
+ candidates = [Path(sys.prefix) / "bin" / "pjm",
72
+ Path(sys.prefix) / "bin" / "projectmem"]
73
+ for guess in candidates:
74
+ if guess.exists():
75
+ return _shell_path(str(guess))
46
76
  return "pjm"
47
77
 
48
78
 
@@ -144,7 +174,7 @@ def install_hooks(hooks_dir: Path) -> None:
144
174
  # Append to existing hook
145
175
  content = content.rstrip("\n") + "\n\n" + snippet
146
176
  else:
147
- content = "#!/usr/bin/env bash\n" + snippet
177
+ content = HOOK_SHEBANG + snippet
148
178
 
149
179
  hook_path.write_text(content, encoding="utf-8")
150
180
  _make_executable(hook_path)
@@ -162,7 +192,7 @@ def install_hooks(hooks_dir: Path) -> None:
162
192
  installed.append("pre-commit")
163
193
  else:
164
194
  precommit_path.write_text(
165
- "#!/usr/bin/env bash\n" + precheck_snippet, encoding="utf-8"
195
+ HOOK_SHEBANG + precheck_snippet, encoding="utf-8"
166
196
  )
167
197
  _make_executable(precommit_path)
168
198
  installed.append("pre-commit")