memory-seed 2.3.0__tar.gz → 2.5.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 (47) hide show
  1. {memory_seed-2.3.0 → memory_seed-2.5.0}/PKG-INFO +15 -3
  2. {memory_seed-2.3.0 → memory_seed-2.5.0}/README.md +14 -2
  3. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/cli.py +2 -0
  4. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/core.py +245 -10
  5. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/mcp_server.py +2 -0
  6. memory_seed-2.5.0/memory_seed/seed/.agents/README.md +118 -0
  7. memory_seed-2.5.0/memory_seed/seed/.agents/content-creator.md +237 -0
  8. memory_seed-2.5.0/memory_seed/seed/.agents/copywriter.md +264 -0
  9. memory_seed-2.5.0/memory_seed/seed/.agents/developer.md +207 -0
  10. memory_seed-2.5.0/memory_seed/seed/.agents/researcher.md +261 -0
  11. memory_seed-2.5.0/memory_seed/seed/.agents/sales-rep.md +242 -0
  12. memory_seed-2.5.0/memory_seed/seed/.agents/solo-founder.md +212 -0
  13. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/agent-rules.md +55 -34
  14. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/hooks/memory-retrieval-check.py +7 -2
  15. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/project-bootstrap.md +165 -3
  16. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/code_search.md +1 -1
  17. memory_seed-2.5.0/memory_seed/seed/.memory-seed/skills/copywriter-conversion.md +94 -0
  18. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/data_architecture.md +1 -1
  19. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/index.md +17 -1
  20. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/local_compilation.md +1 -1
  21. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/memory_consolidation.md +1 -1
  22. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/memory_doctor.md +1 -1
  23. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/release_publishing.md +1 -1
  24. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/skills/security_triage.md +1 -1
  25. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/AGENTS.md +1 -1
  26. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/CLAUDE.md +1 -1
  27. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/GEMINI.md +1 -1
  28. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/semantic_cache.py +4 -0
  29. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed.egg-info/PKG-INFO +15 -3
  30. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed.egg-info/SOURCES.txt +8 -0
  31. {memory_seed-2.3.0 → memory_seed-2.5.0}/pyproject.toml +9 -1
  32. {memory_seed-2.3.0 → memory_seed-2.5.0}/tests/test_memory_seed.py +298 -27
  33. {memory_seed-2.3.0 → memory_seed-2.5.0}/tests/test_session_schema.py +9 -5
  34. {memory_seed-2.3.0 → memory_seed-2.5.0}/LICENSE +0 -0
  35. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/__init__.py +0 -0
  36. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/mcp_validate.py +0 -0
  37. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/archive/.gitkeep +0 -0
  38. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/hooks/session-log-check.py +0 -0
  39. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed/seed/.memory-seed/sessions/.gitkeep +0 -0
  40. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed.egg-info/dependency_links.txt +0 -0
  41. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed.egg-info/entry_points.txt +0 -0
  42. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed.egg-info/requires.txt +0 -0
  43. {memory_seed-2.3.0 → memory_seed-2.5.0}/memory_seed.egg-info/top_level.txt +0 -0
  44. {memory_seed-2.3.0 → memory_seed-2.5.0}/setup.cfg +0 -0
  45. {memory_seed-2.3.0 → memory_seed-2.5.0}/tests/test_mcp_server.py +0 -0
  46. {memory_seed-2.3.0 → memory_seed-2.5.0}/tests/test_mcp_validation.py +0 -0
  47. {memory_seed-2.3.0 → memory_seed-2.5.0}/tests/test_semantic_cache.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memory-seed
3
- Version: 2.3.0
3
+ Version: 2.5.0
4
4
  Summary: Portable local memory seed for file-reading AI coding agents
5
5
  Author: Jean Nathan Tshibuyi
6
6
  License: MIT
@@ -135,7 +135,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
135
135
 
136
136
  | Agent or client | Support path |
137
137
  | --- | --- |
138
- | Codex | Starts from `AGENTS.md`; can use MCP when the client supports stdio MCP servers. |
138
+ | Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
139
139
  | Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
140
140
  | Gemini CLI | Starts from `GEMINI.md`. |
141
141
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
@@ -365,7 +365,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
365
365
 
366
366
  Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
367
367
 
368
- **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
368
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
369
+
370
+ > **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
371
+
372
+ > **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
369
373
 
370
374
  If you are configuring the server manually, run it over stdio:
371
375
 
@@ -422,6 +426,14 @@ memory_get_chunk(chunk_id, cwd=".")
422
426
 
423
427
  The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
424
428
 
429
+ ### Performance characteristics
430
+
431
+ `memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
432
+
433
+ Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
434
+
435
+ When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
436
+
425
437
  Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
426
438
 
427
439
  For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
@@ -114,7 +114,7 @@ The result is a lightweight memory workflow you can understand, commit, review,
114
114
 
115
115
  | Agent or client | Support path |
116
116
  | --- | --- |
117
- | Codex | Starts from `AGENTS.md`; can use MCP when the client supports stdio MCP servers. |
117
+ | Codex | Starts from `AGENTS.md`; MCP server auto-registered in `.codex/config.toml` (loads once the project directory is trusted). |
118
118
  | Claude Code | Starts from `CLAUDE.md`; MCP server auto-registered via `uvx --from memory-seed`. |
119
119
  | Gemini CLI | Starts from `GEMINI.md`. |
120
120
  | Other file-reading agents | Start from `AGENTS.md` and follow nearest `.memory-seed/` runtime discovery. |
@@ -344,7 +344,11 @@ For the version distinction (`pip show memory-seed` reports the package version;
344
344
 
345
345
  Memory Seed also includes a lightweight MCP server that lets agents search local session memory through structured tool calls instead of shelling out to broad compact summaries.
346
346
 
347
- **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's config — `.claude/settings.json` (Claude Code), `.cursor/mcp.json` (Cursor), and `.gemini/settings.json` (Gemini CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
347
+ **Auto-registration:** `memory-seed init` and `memory-seed update` automatically register `uvx --from memory-seed memory-seed-mcp --stdio` in each supported vendor's MCP config — `.mcp.json` at the project root (Claude Code), `.cursor/mcp.json` (Cursor), `.gemini/settings.json` (Gemini CLI), and `.codex/config.toml` (Codex CLI). No manual config is needed for projects initialised with Memory Seed. The `uvx --from` form is used so the command works regardless of whether `~/.local/bin` is on the agent's PATH.
348
+
349
+ > **Claude Code reads project-scope MCP servers from `.mcp.json`, not `.claude/settings.json`** — the latter is for hooks and permissions only. Versions 2.2.0–2.3.0 wrote the server into `.claude/settings.json`, where Claude Code silently ignored it; `memory-seed update` now writes `.mcp.json` and removes the dead entry. Restart Claude Code and approve the project server, then confirm with `claude mcp list`.
350
+
351
+ > **Codex loads a project `.codex/config.toml` only for *trusted* directories.** Memory Seed writes the `[mcp_servers.memory-seed]` table there, but Codex ignores it until you trust the project (Codex prompts on first use of a directory, or set trust in Codex settings). After trusting, confirm with `codex mcp list`. `memory-seed doctor` warns if Codex hooks are present without this registration. If you hand-wrote the `memory-seed` entry in a non-standard TOML form (dotted keys, an inline table, or a header with a trailing comment) and it is outdated, `memory-seed update` will not auto-migrate it — `memory-seed doctor` flags it as needing a manual fix instead of silently leaving stale settings in place.
348
352
 
349
353
  If you are configuring the server manually, run it over stdio:
350
354
 
@@ -401,6 +405,14 @@ memory_get_chunk(chunk_id, cwd=".")
401
405
 
402
406
  The ranking engine stays local and CPU-friendly. MCP search uses a Model2Vec static embedding provider by default with the general-purpose `minishlab/potion-base-8M` model, combines semantic score with lexical and metadata scoring, then applies recency. If Model2Vec or the model cannot load or score a query, the server falls back to lexical, metadata, and recency ranking without failing the request. Use `--no-semantic` on `memory-seed-mcp --stdio` or `semantic_enabled=false` in `memory_search` to force fallback behavior.
403
407
 
408
+ ### Performance characteristics
409
+
410
+ `memory_search` is a relevance-and-recall tool, not a faster `grep`. A plain `grep` will out-scan it on raw exact-match throughput; the search wins instead on *semantic recall* over session history (surfacing relevant entries that lack the literal query words) and on *agent-token efficiency* (returning a small ranked set of self-contained chunks with stable `chunk_id`s, so an agent fetches only the one or two full entries worth reading). The two are complementary: use `memory_search` for "what did we decide and why," and `grep` for exact-string scans across the whole repo.
411
+
412
+ Per-query latency, measured in-process on this repo (81 chunks across the session logs), is roughly **30 ms**, of which about **22 ms is reading and parsing the session `.md` files** — the search re-reads and re-parses every `sessions/*.md` on each call, with no persistent chunk or vector cache — and the embed + cosine + rank step adds only a few ms on top. Cold start adds a one-time cost on the *first ever* call on a machine: the Model2Vec weights download into the local HuggingFace cache (tens of MB); afterwards the static model loads in a few ms. Because the static model has no transformer forward pass, the dominant cost is file I/O, so per-query time grows linearly with total session-log size rather than with model complexity.
413
+
414
+ When driving the server through an MCP client (Claude Code, Cursor, Gemini), the latency you actually perceive is dominated by one-time startup, not per-query work: spawning `uvx --from memory-seed memory-seed-mcp` resolves and may install the package into an ephemeral environment the first time the server launches in a session. Once the server is up, each `memory_search` is the ~30 ms compute above plus a small JSON-RPC round-trip. At current log sizes there is no need to optimize; should logs grow large enough that the ~22 ms parse cost becomes noticeable, caching parsed chunks and their vectors keyed by file modification time would remove most of the per-query cost.
415
+
404
416
  Session entries should include a YAML metadata block with `entry_id`, `user_initials`, `agent_type`, `project_path`, and `subproject_path`. Session entry headings may include optional minute-level timestamps, such as `## 2026-05-19 20:42 - Durable memory consolidation`. Session filenames stay date-only. Timestamped headings are backward compatible with older untimed headings and are exposed as `entry_datetime` in MCP search results when present.
405
417
 
406
418
  For human-validatable search behavior, see the fixture-style tests in `tests/test_mcp_server.py`. They assert that specific queries return expected dated session entries first and include enough evidence for manual review.
@@ -118,6 +118,8 @@ def main(argv: list[str] | None = None) -> int:
118
118
  print("Memory Seed bootstrap is complete.")
119
119
  else:
120
120
  print("Memory Seed bootstrap is incomplete.")
121
+ for warning in result.warnings:
122
+ print(f"Warning: {warning}")
121
123
  if result.ok:
122
124
  return 0
123
125
  for missing in result.missing:
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import json
4
4
  import re
5
5
  import hashlib
6
+ import tomllib
6
7
  from dataclasses import dataclass, field
7
8
  from datetime import datetime, timedelta
8
9
  from pathlib import Path
@@ -10,7 +11,7 @@ from pathlib import Path
10
11
 
11
12
  PACKAGE_ROOT = Path(__file__).resolve().parent
12
13
  SEED_ROOT = PACKAGE_ROOT / "seed"
13
- VERSION = "2.3"
14
+ VERSION = "2.5"
14
15
  MEMORY_DIR_NAME = ".memory-seed"
15
16
  LEGACY_MEMORY_DIR_NAME = ".AGENTS"
16
17
  BACKUP_IGNORE_ENTRY = ".memory-seed/backups/"
@@ -49,6 +50,7 @@ class DoctorResult:
49
50
  missing: list[str] = field(default_factory=list)
50
51
  version_mismatches: list[dict[str, str]] = field(default_factory=list)
51
52
  bootstrap_missing: list[str] = field(default_factory=list)
53
+ warnings: list[str] = field(default_factory=list)
52
54
 
53
55
 
54
56
  @dataclass
@@ -63,6 +65,13 @@ SEED_FILES = [
63
65
  SeedFile(SEED_ROOT / "AGENTS.md", "AGENTS.md"),
64
66
  SeedFile(SEED_ROOT / "CLAUDE.md", "CLAUDE.md"),
65
67
  SeedFile(SEED_ROOT / "GEMINI.md", "GEMINI.md"),
68
+ SeedFile(SEED_ROOT / ".agents" / "README.md", ".agents/README.md"),
69
+ SeedFile(SEED_ROOT / ".agents" / "developer.md", ".agents/developer.md"),
70
+ SeedFile(SEED_ROOT / ".agents" / "content-creator.md", ".agents/content-creator.md"),
71
+ SeedFile(SEED_ROOT / ".agents" / "researcher.md", ".agents/researcher.md"),
72
+ SeedFile(SEED_ROOT / ".agents" / "sales-rep.md", ".agents/sales-rep.md"),
73
+ SeedFile(SEED_ROOT / ".agents" / "solo-founder.md", ".agents/solo-founder.md"),
74
+ SeedFile(SEED_ROOT / ".agents" / "copywriter.md", ".agents/copywriter.md"),
66
75
  SeedFile(
67
76
  SEED_ROOT / MEMORY_DIR_NAME / "agent-rules.md",
68
77
  ".memory-seed/agent-rules.md",
@@ -76,6 +85,10 @@ SEED_FILES = [
76
85
  SEED_ROOT / MEMORY_DIR_NAME / "skills" / "security_triage.md",
77
86
  ".memory-seed/skills/security_triage.md",
78
87
  ),
88
+ SeedFile(
89
+ SEED_ROOT / MEMORY_DIR_NAME / "skills" / "copywriter-conversion.md",
90
+ ".memory-seed/skills/copywriter-conversion.md",
91
+ ),
79
92
  SeedFile(
80
93
  SEED_ROOT / MEMORY_DIR_NAME / "skills" / "code_search.md",
81
94
  ".memory-seed/skills/code_search.md",
@@ -350,14 +363,19 @@ def _merge_cursor_retrieval_hook(target_root: Path) -> bool:
350
363
 
351
364
 
352
365
  def _merge_claude_mcp(target_root: Path) -> bool:
353
- """Upsert the memory-seed-mcp stdio server entry in .claude/settings.json."""
354
- settings_path = target_root / ".claude" / "settings.json"
355
- expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS, "type": "stdio"}
366
+ """Upsert the memory-seed-mcp stdio server entry in the project-root .mcp.json.
367
+
368
+ Claude Code discovers project-scope MCP servers from .mcp.json, not from
369
+ .claude/settings.json (that key is silently ignored by Claude Code).
370
+ _strip_claude_settings_mcp removes the legacy settings.json entry.
371
+ """
372
+ mcp_path = target_root / ".mcp.json"
373
+ expected = {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
356
374
 
357
375
  data: dict = {}
358
- if settings_path.exists():
376
+ if mcp_path.exists():
359
377
  try:
360
- with open(settings_path) as f:
378
+ with open(mcp_path) as f:
361
379
  data = json.load(f)
362
380
  except (json.JSONDecodeError, OSError):
363
381
  data = {}
@@ -371,7 +389,44 @@ def _merge_claude_mcp(target_root: Path) -> bool:
371
389
 
372
390
  data.setdefault("mcpServers", {})[_MCP_SERVER_KEY] = expected
373
391
 
374
- settings_path.parent.mkdir(parents=True, exist_ok=True)
392
+ mcp_path.parent.mkdir(parents=True, exist_ok=True)
393
+ with open(mcp_path, "w") as f:
394
+ json.dump(data, f, indent=2)
395
+ f.write("\n")
396
+
397
+ return True
398
+
399
+
400
+ def _strip_claude_settings_mcp(target_root: Path) -> bool:
401
+ """Remove the legacy memory-seed MCP entry from .claude/settings.json.
402
+
403
+ Versions 2.2.0-2.3.0 wrote the server into .claude/settings.json > mcpServers,
404
+ which Claude Code never reads. Now that the server lives in .mcp.json, drop the
405
+ dead entry so it does not mislead. Only our own entry is removed; a foreign
406
+ server under the same key is left untouched.
407
+ """
408
+ settings_path = target_root / ".claude" / "settings.json"
409
+ if not settings_path.exists():
410
+ return False
411
+ try:
412
+ with open(settings_path) as f:
413
+ data = json.load(f)
414
+ except (json.JSONDecodeError, OSError):
415
+ return False
416
+
417
+ servers = data.get("mcpServers")
418
+ if not isinstance(servers, dict) or _MCP_SERVER_KEY not in servers:
419
+ return False
420
+
421
+ existing = servers.get(_MCP_SERVER_KEY, {})
422
+ is_ours = existing.get("command") in _OWN_MCP_COMMANDS or "memory-seed-mcp" in existing.get("args", [])
423
+ if not is_ours:
424
+ return False # foreign server under the same key; leave it alone
425
+
426
+ del servers[_MCP_SERVER_KEY]
427
+ if not servers:
428
+ del data["mcpServers"]
429
+
375
430
  with open(settings_path, "w") as f:
376
431
  json.dump(data, f, indent=2)
377
432
  f.write("\n")
@@ -439,6 +494,120 @@ def _merge_gemini_mcp(target_root: Path) -> bool:
439
494
  return True
440
495
 
441
496
 
497
+ # Header line for our entry in .codex/config.toml. Codex accepts both the bare
498
+ # and quoted single-segment table-key forms; we write the bare form.
499
+ _CODEX_MCP_HEADER_RE = re.compile(
500
+ r'^\[mcp_servers\.(?:memory-seed|"memory-seed")\]\s*$'
501
+ )
502
+
503
+
504
+ def _codex_expected() -> dict:
505
+ """The MCP table we want present under [mcp_servers.memory-seed]."""
506
+ return {"command": _MCP_SERVER_COMMAND, "args": _MCP_SERVER_ARGS}
507
+
508
+
509
+ def _codex_standard_header_index(lines: list[str]) -> int | None:
510
+ """Index of the standard ``[mcp_servers.memory-seed]`` header line, or None.
511
+
512
+ The in-place stale-update path can only rewrite an entry written with this
513
+ header form. Shared by _merge_codex_mcp (to decide whether it can migrate)
514
+ and _codex_mcp_status (to decide stale-fixable vs stale-manual), so the two
515
+ always agree on what counts as auto-fixable.
516
+ """
517
+ return next(
518
+ (i for i, ln in enumerate(lines) if _CODEX_MCP_HEADER_RE.match(ln)),
519
+ None,
520
+ )
521
+
522
+
523
+ def _render_codex_mcp_block() -> str:
524
+ """Render our fixed [mcp_servers.memory-seed] TOML table.
525
+
526
+ args is a TOML array of strings, which is JSON-compatible, so json.dumps
527
+ produces valid TOML for it.
528
+ """
529
+ return (
530
+ f"[mcp_servers.{_MCP_SERVER_KEY}]\n"
531
+ f'command = "{_MCP_SERVER_COMMAND}"\n'
532
+ f"args = {json.dumps(_MCP_SERVER_ARGS)}\n"
533
+ )
534
+
535
+
536
+ def _merge_codex_mcp(target_root: Path) -> bool:
537
+ """Upsert the memory-seed-mcp stdio server in the project .codex/config.toml.
538
+
539
+ Codex reads project-scoped MCP servers from .codex/config.toml under
540
+ [mcp_servers.<name>] (trusted projects only). This is a zero-dependency text
541
+ upsert: tomllib (stdlib, Python >=3.11) is used only to *inspect* current
542
+ state; writes are line-based so existing content and comments are preserved.
543
+
544
+ Returns True if the file was written, False if already current.
545
+
546
+ Known limitation (in-place stale-entry update only): rewriting a present-but-
547
+ outdated entry while preserving comments relies on finding the standard
548
+ ``[mcp_servers.memory-seed]`` header line. Detection itself is robust (tomllib
549
+ parses semantically), but if a user *hand-wrote* the entry in a form that has
550
+ no such header line — dotted keys (``mcp_servers.memory-seed.command = ...``),
551
+ an inline subtable under ``[mcp_servers]``, a fully inline
552
+ ``mcp_servers = { ... }``, or a header with a trailing comment / leading
553
+ indentation — and the entry is stale, this no-ops (returns False) rather than
554
+ risk a duplicate-key / invalid-TOML write. The no-op is intentionally not
555
+ silent: ``doctor`` classifies this case via _codex_mcp_status as a
556
+ ``stale-manual`` warning telling the user to fix it by hand. Memory Seed only
557
+ ever writes the standard header form, so this path is only reachable through
558
+ manual edits.
559
+ """
560
+ config_path = target_root / ".codex" / "config.toml"
561
+ block = _render_codex_mcp_block()
562
+
563
+ text = ""
564
+ parsed: dict = {}
565
+ if config_path.exists():
566
+ try:
567
+ text = config_path.read_text(encoding="utf-8")
568
+ parsed = tomllib.loads(text)
569
+ except (tomllib.TOMLDecodeError, OSError):
570
+ text = ""
571
+ parsed = {}
572
+
573
+ existing = parsed.get("mcp_servers", {}).get(_MCP_SERVER_KEY, {})
574
+ if existing == _codex_expected():
575
+ return False
576
+ if existing:
577
+ is_ours = (
578
+ existing.get("command") in _OWN_MCP_COMMANDS
579
+ or "memory-seed-mcp" in existing.get("args", [])
580
+ )
581
+ if not is_ours:
582
+ return False # a different server holds this key; don't overwrite
583
+
584
+ if not existing:
585
+ # Append our block, preserving everything above it.
586
+ new_text = text
587
+ if new_text and not new_text.endswith("\n"):
588
+ new_text += "\n"
589
+ if new_text:
590
+ new_text += "\n"
591
+ new_text += block
592
+ else:
593
+ # Stale entry: replace just our table's lines (header to next table/EOF).
594
+ lines = text.splitlines(keepends=True)
595
+ start = _codex_standard_header_index(lines)
596
+ if start is None:
597
+ # No standard header to anchor the rewrite. Don't risk a duplicate
598
+ # key; leave it for the user. doctor() flags this as stale-manual.
599
+ return False
600
+ end = start + 1
601
+ while end < len(lines) and not lines[end].lstrip().startswith("["):
602
+ end += 1
603
+ replacement = block if block.endswith("\n") else block + "\n"
604
+ new_text = "".join(lines[:start]) + replacement + "".join(lines[end:])
605
+
606
+ config_path.parent.mkdir(parents=True, exist_ok=True)
607
+ config_path.write_text(new_text, encoding="utf-8")
608
+ return True
609
+
610
+
442
611
  def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = False) -> InitResult:
443
612
  target_root = Path(cwd).resolve()
444
613
  planned = [seed_file.destination for seed_file in SEED_FILES]
@@ -484,9 +653,11 @@ def init_project(cwd: str | Path = ".", dry_run: bool = False, force: bool = Fal
484
653
  (_merge_codex_retrieval_hook, ".codex/hooks.json"),
485
654
  (_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
486
655
  (_merge_gemini_retrieval_hook, ".gemini/settings.json"),
487
- (_merge_claude_mcp, ".claude/settings.json"),
656
+ (_merge_claude_mcp, ".mcp.json"),
657
+ (_strip_claude_settings_mcp, ".claude/settings.json"),
488
658
  (_merge_cursor_mcp, ".cursor/mcp.json"),
489
659
  (_merge_gemini_mcp, ".gemini/settings.json"),
660
+ (_merge_codex_mcp, ".codex/config.toml"),
490
661
  )
491
662
  for merge, destination in hook_merges:
492
663
  if merge(target_root) and destination not in created:
@@ -551,9 +722,11 @@ def update_project(cwd: str | Path = ".", dry_run: bool = False) -> InitResult:
551
722
  (_merge_codex_retrieval_hook, ".codex/hooks.json"),
552
723
  (_merge_cursor_retrieval_hook, ".cursor/hooks.json"),
553
724
  (_merge_gemini_retrieval_hook, ".gemini/settings.json"),
554
- (_merge_claude_mcp, ".claude/settings.json"),
725
+ (_merge_claude_mcp, ".mcp.json"),
726
+ (_strip_claude_settings_mcp, ".claude/settings.json"),
555
727
  (_merge_cursor_mcp, ".cursor/mcp.json"),
556
728
  (_merge_gemini_mcp, ".gemini/settings.json"),
729
+ (_merge_codex_mcp, ".codex/config.toml"),
557
730
  )
558
731
  for merge, destination in hook_merges:
559
732
  if merge(target_root) and destination not in created:
@@ -581,6 +754,8 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
581
754
 
582
755
  if not candidate.suffix == ".md":
583
756
  continue
757
+ if seed_file.destination.startswith(".agents/"):
758
+ continue # agent personas are project-local; not version-tracked control plane
584
759
  actual = _read_memory_system_version(candidate)
585
760
  if actual != VERSION:
586
761
  version_mismatches.append(
@@ -600,6 +775,28 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
600
775
  control_plane_ok = not missing and not version_mismatches
601
776
  bootstrap_complete = not bootstrap_missing
602
777
 
778
+ warnings: list[str] = []
779
+ codex_status = _codex_mcp_status(target_root)
780
+ if (target_root / ".codex" / "hooks.json").exists() and codex_status == "absent":
781
+ warnings.append(
782
+ "Codex hooks are installed but .codex/config.toml has no memory-seed MCP "
783
+ "entry. Run `memory-seed update`, then trust this directory in Codex so it "
784
+ "loads the project MCP server (memory_search / memory_get_chunk)."
785
+ )
786
+ elif codex_status == "stale-fixable":
787
+ warnings.append(
788
+ "Codex .codex/config.toml has an outdated memory-seed MCP entry. Run "
789
+ "`memory-seed update` to migrate it to `uvx --from memory-seed "
790
+ "memory-seed-mcp --stdio`."
791
+ )
792
+ elif codex_status == "stale-manual":
793
+ warnings.append(
794
+ "Codex .codex/config.toml has an outdated memory-seed MCP entry written in "
795
+ "a non-standard TOML form that `memory-seed update` cannot safely auto-fix. "
796
+ 'Set it by hand to: command = "uvx", args = ["--from", "memory-seed", '
797
+ '"memory-seed-mcp", "--stdio"].'
798
+ )
799
+
603
800
  return DoctorResult(
604
801
  ok=control_plane_ok and bootstrap_complete,
605
802
  control_plane_ok=control_plane_ok,
@@ -607,9 +804,47 @@ def doctor(cwd: str | Path = ".") -> DoctorResult:
607
804
  missing=missing,
608
805
  version_mismatches=version_mismatches,
609
806
  bootstrap_missing=bootstrap_missing,
807
+ warnings=warnings,
610
808
  )
611
809
 
612
810
 
811
+ def _codex_mcp_status(target_root: Path) -> str:
812
+ """Classify our memory-seed entry in .codex/config.toml.
813
+
814
+ Returns one of:
815
+ "absent" - no entry (or no/unparseable file)
816
+ "current" - present and matches the expected uvx command + args
817
+ "foreign" - present but owned by a different server
818
+ "stale-fixable" - ours but outdated, written with a standard header that
819
+ `memory-seed update` can auto-migrate
820
+ "stale-manual" - ours but outdated, written in a form with no standard
821
+ header line, so update no-ops and the user must edit it
822
+ """
823
+ config_path = target_root / ".codex" / "config.toml"
824
+ if not config_path.exists():
825
+ return "absent"
826
+ try:
827
+ text = config_path.read_text(encoding="utf-8")
828
+ parsed = tomllib.loads(text)
829
+ except (tomllib.TOMLDecodeError, OSError):
830
+ return "absent"
831
+
832
+ existing = parsed.get("mcp_servers", {}).get(_MCP_SERVER_KEY, {})
833
+ if not existing:
834
+ return "absent"
835
+ if existing == _codex_expected():
836
+ return "current"
837
+ is_ours = (
838
+ existing.get("command") in _OWN_MCP_COMMANDS
839
+ or "memory-seed-mcp" in existing.get("args", [])
840
+ )
841
+ if not is_ours:
842
+ return "foreign"
843
+ if _codex_standard_header_index(text.splitlines()) is not None:
844
+ return "stale-fixable"
845
+ return "stale-manual"
846
+
847
+
613
848
  def compact_sessions(
614
849
  cwd: str | Path = ".",
615
850
  days: int = 7,
@@ -702,7 +937,7 @@ def _is_runtime_local_file(destination: str) -> bool:
702
937
  return (
703
938
  destination.startswith(f"{MEMORY_DIR_NAME}/")
704
939
  and destination not in reusable_runtime_files
705
- )
940
+ ) or destination.startswith(".agents/")
706
941
 
707
942
 
708
943
  def _archive_replaced_control_plane_file(
@@ -241,6 +241,7 @@ def _ranked_to_dict(result: RankedMemoryChunk) -> dict[str, Any]:
241
241
  "entry_id": chunk.entry_id,
242
242
  "user_initials": chunk.user_initials,
243
243
  "agent_type": chunk.agent_type,
244
+ "agent_name": chunk.agent_name,
244
245
  "project_path": chunk.project_path,
245
246
  "subproject_path": chunk.subproject_path,
246
247
  "entry_title": chunk.entry_title,
@@ -270,6 +271,7 @@ def _chunk_to_dict(chunk: MemoryChunk) -> dict[str, Any]:
270
271
  "entry_id": chunk.entry_id,
271
272
  "user_initials": chunk.user_initials,
272
273
  "agent_type": chunk.agent_type,
274
+ "agent_name": chunk.agent_name,
273
275
  "project_path": chunk.project_path,
274
276
  "subproject_path": chunk.subproject_path,
275
277
  "entry_title": chunk.entry_title,
@@ -0,0 +1,118 @@
1
+ # .agents/ — Agent Persona Library
2
+
3
+ This folder ships with every `memory-seed init` and provides vendor-neutral persona templates that any LLM can inhabit. Each persona defines an identity, operating rules, memory protocol, and session discipline for a specific role.
4
+
5
+ ---
6
+
7
+ ## Available Personas
8
+
9
+ | File | Agent Name | Role |
10
+ |------|-----------|------|
11
+ | `developer.md` | `developer` | Software Engineer / Staff IC |
12
+ | `content-creator.md` | `content-creator` | Content Strategist / Writer |
13
+ | `researcher.md` | `researcher` | Research Engine / Knowledge Architect |
14
+ | `sales-rep.md` | `sales-rep` | Sales Operator / Pipeline Manager |
15
+ | `solo-founder.md` | `solo-founder` | Co-founder / Generalist |
16
+
17
+ ---
18
+
19
+ ## Activation and Deactivation
20
+
21
+ Personas are controlled by `.agents/_registry.yaml`. Set `status: active` to enable a persona; `status: inactive` to disable it.
22
+
23
+ Bootstrap generates `_registry.yaml` when you first run `memory-seed init` in a project. Edit it at any time to switch personas.
24
+
25
+ **Example registry:**
26
+ ```yaml
27
+ agents:
28
+ developer:
29
+ file: developer.md
30
+ role: Software Engineer / Staff IC
31
+ status: active
32
+ solo-founder:
33
+ file: solo-founder.md
34
+ role: Co-founder / Generalist
35
+ status: inactive
36
+ ```
37
+
38
+ When an agent starts a session, it reads `_registry.yaml`, loads all `status: active` persona files, and applies those rules alongside `agent-rules.md` and `policy.md`.
39
+
40
+ **Multiple active personas:** Allowed. The persona most relevant to the current task governs. When ambiguous, the first active entry in `_registry.yaml` is the default.
41
+
42
+ ---
43
+
44
+ ## Adding a New Persona
45
+
46
+ **Quick path (existing persona as starting point):**
47
+
48
+ 1. Copy any existing persona file: `cp developer.md data-scientist.md`
49
+ 2. Save it to `.agents/data-scientist.md`
50
+ 3. At the next session start, the agent detects the unregistered file and runs the onboarding flow automatically:
51
+ - Adds or corrects YAML frontmatter (`agent_name`, `memory_protocol`, `vendor_neutral`, `tags`)
52
+ - Rewrites the Memory Protocol section to use `.memory-seed/` paths (if it references a separate `memory/` folder)
53
+ - Adds `## Project Adaptations` and `## Skills` sections if missing
54
+ - Runs personalization: asks for entity name (or generates one), confirms user name, asks for business name
55
+ - Routes relevant skills from `.memory-seed/skills/` and offers to generate missing ones
56
+ - Adds the persona to `_registry.yaml` with `status: active`
57
+
58
+ **Starting from scratch:**
59
+
60
+ Write a plain Markdown file describing the role. The onboarding flow will handle all formatting. The minimum content needed:
61
+ ```markdown
62
+ # [Role] Operating System
63
+
64
+ ## I. Identity
65
+
66
+ You are [describe the role and its purpose].
67
+
68
+ ## III. Operating Rules
69
+
70
+ [Domain-specific rules and standards for this role]
71
+ ```
72
+
73
+ The agent fills in the Memory Protocol, Skills, Project Adaptations, and Persona Evolution sections during onboarding.
74
+
75
+ The `.agents/` folder is project-local: `memory-seed update` never overwrites it once created, so custom personas and their evolved content survive upgrades.
76
+
77
+ ---
78
+
79
+ ## Persona Evolution (Lessons Learned)
80
+
81
+ Personas grow smarter as the project progresses. At session end, the active agent may propose changes to the persona file based on patterns observed during the session. The flow:
82
+
83
+ 1. Agent drafts proposed change(s): what to add/change/remove and why (evidence from this session).
84
+ 2. Agent presents proposals to the user for approval. **No edits happen until the user approves.**
85
+ 3. On approval, the agent:
86
+ - Applies the edit to the relevant section of the persona file.
87
+ - Appends an entry to the `## Project Adaptations` section at the bottom of the persona file.
88
+ - Appends a session log entry (`agent_name: <slug>`, D/R/F + "Signed: user approved YYYY-MM-DD HH:MM").
89
+
90
+ **Project Adaptations entry format:**
91
+ ```markdown
92
+ ### YYYY-MM-DD — Short description of what changed
93
+ Session: ms-<entry_id> | Approved by: <user_initials>
94
+ Section changed: [section name]
95
+ Rationale: [one-line reason from session]
96
+ ```
97
+
98
+ This creates three layers of traceability: the session log decision record, the in-file changelog, and the git diff.
99
+
100
+ If no lessons emerged, skip the proposal — no empty prompts.
101
+
102
+ ---
103
+
104
+ ## Memory Protocol
105
+
106
+ Each persona's Memory Protocol section points to the memory-seed runtime:
107
+ - `.memory-seed/index.md` — active state and current focus
108
+ - `.memory-seed/sessions/` (last 2 files) — recent decisions and open threads
109
+ - `.memory-seed/policy.md` — behavioral constraints and preferences
110
+ - `memory_search` MCP — historical context beyond the last 2 sessions
111
+
112
+ Persona session entries use `agent_name: <slug>` in the entry YAML block (see `agent-rules.md` for the full format). This lets `memory_search` filter entries by persona.
113
+
114
+ ---
115
+
116
+ ## Vendor Neutrality
117
+
118
+ Persona files use `.memory-seed/` paths and plain Markdown conventions — no LLM-specific APIs, directives, or syntax. Any file-reading AI coding agent (Claude, Gemini, Codex, GPT-4, or a future model) can read and apply these files.