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.
- {projectmem-0.3.2 → projectmem-0.3.3}/CHANGELOG.md +40 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/PKG-INFO +50 -5
- {projectmem-0.3.2 → projectmem-0.3.3}/README.md +49 -4
- {projectmem-0.3.2 → projectmem-0.3.3}/pyproject.toml +1 -1
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/__init__.py +1 -1
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/cli.py +11 -1
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/brief.py +2 -1
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/context.py +10 -1
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/dashboard.py +2 -1
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/doctor.py +16 -15
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/global_cmd.py +4 -3
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/hooks.py +38 -8
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/init.py +70 -22
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/precheck.py +20 -6
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/project.py +12 -11
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/score.py +11 -10
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/stats.py +4 -3
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/watch.py +3 -2
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/wrap.py +4 -3
- projectmem-0.3.3/src/projectmem/glyphs.py +139 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/mcp_server.py +43 -11
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/staleness.py +80 -10
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/storage.py +15 -0
- projectmem-0.3.3/tests/test_agent_guidance.py +280 -0
- projectmem-0.3.3/tests/test_staleness_performance.py +146 -0
- projectmem-0.3.3/tests/test_windows_console_encoding.py +178 -0
- projectmem-0.3.3/tests/test_windows_hooks_and_context.py +204 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/.github/FUNDING.yml +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/.gitignore +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/LICENSE +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/SECURITY.md +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/TUTORIAL.md +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/llms.txt +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/__init__.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/attempt.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/auto_capture.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/backfill.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/decision.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/export.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/fix.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/instructions.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/log.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/map.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/note.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/plan.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/regenerate.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/search.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/show.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/commands/visualize.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/global_memory.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/models.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/project_registry.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/redaction.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/resolver.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/search.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/structure.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/src/projectmem/summary.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/conftest.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_client_config_discovery.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_dashboard.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_global_mcp.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_hooks_path.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_init.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_init_ux.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_location_parsing.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_log.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_mcp_server.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_plan.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_redaction.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_search.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_showoff.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_structure.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_summary.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_v014_features.py +0 -0
- {projectmem-0.3.2 → projectmem-0.3.3}/tests/test_visualize.py +0 -0
- {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.
|
|
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.
|
|
39
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
8
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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"
|
|
349
|
-
typer.secho(f"
|
|
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"
|
|
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
|
|
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"
|
|
364
|
+
typer.secho(f" {OK} Removed {len(stale)} from the registry (repos untouched)", fg=typer.colors.GREEN)
|
|
364
365
|
else:
|
|
365
|
-
typer.secho("\n
|
|
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
|
|
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
|
|
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
|
|
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}{'
|
|
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}{'
|
|
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}{'
|
|
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.
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 =
|
|
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
|
-
|
|
195
|
+
HOOK_SHEBANG + precheck_snippet, encoding="utf-8"
|
|
166
196
|
)
|
|
167
197
|
_make_executable(precommit_path)
|
|
168
198
|
installed.append("pre-commit")
|