feat(context): PR-B — mm context install skill + lockfile + concurrency#623
Merged
feat(context): PR-B — mm context install skill + lockfile + concurrency#623
Conversation
Implements ADR-0008 PR-B: install a wiki asset into a project tree with a recorded commit pin and concurrency-safe lockfile. Surface - mm context install skill <name> — copytree snapshot of ~/.memtomem-wiki/skills/<name>/ into <project>/.memtomem/skills/<name>/ pinned to wiki HEAD. PR-B is skill-only; agents/commands land in PR-C alongside override resolution. The Choice widens to one line then. - <project>/.memtomem/lock.json — version 1, full 40-char wiki_commit, microsecond-precision installed_at. Reads preserve unknown fields per ADR-0008 (round-trip through plain dict; no strict schema strip). Concurrency - Lock window covers only the lockfile JSON load → mutate → atomic_write triple. The slow copytree runs unlocked. The wiki commit is pinned at the start of install so a concurrent git pull cannot drift the recorded wiki_commit from the bytes copied. - _file_lock + _lock_path_for hoisted from projects.py to _atomic.py so both KnownProjectsStore and Lockfile share the PR-#548 sidecar pattern. Refusal posture - AlreadyInstalledError fires on (lockfile_entry OR dest_exists), not AND. Catches "user wiped dest only" and "dest exists, lockfile damaged" silent-overwrite holes. Forward-protects ADR Invariant 2 without depending on PR-D's mtime/dirty detection. - LockfileVersionError raises in strict (write) paths; load(strict=False) returns the raw dict so PR-D's mm context status can render "version 99 (unknown), this tool supports v1". Refactors - copy_tree_atomic hoisted from skills.py to _atomic.py as a public helper, with defensive .git skip and a returned file count. - git_identity / wiki_root fixtures moved to tests/_wiki_fixtures.py and re-exported from conftest.py so test_wiki_store.py, test_wiki_cmd.py, and test_context_install.py share one definition. Tests - test_lockfile.py — load recovery (missing/invalid/unknown-version), upsert round-trip with unknown-field preservation, 8-process spawn concurrency. - test_context_install.py — copies tree, records lockfile, .git skip, Invariant 3 message, asset-missing, four AlreadyInstalled cases (AND case + both OR-only cases), invalid name, two-skill spawn concurrency, full CLI surface incl. classified error messages. Out of scope - PR-C: agent/command install, override resolution, OVERRIDE_FORMATS. - PR-D: mm context update, --force, .bak, mm context status, install --all, mtime/dirty detection. - v2: --commit <ref>, directory-level os.replace atomic snapshot. Co-Authored-By: Claude <[email protected]>
Correctness
- Replace `asset_type.rstrip('s')` with `removesuffix('s')` — rstrip
strips characters in a set, not a suffix; lucky for the three current
asset types but PR-C `agents`/`commands` widening is the surfacing
trigger.
- `copy_tree_atomic` skips symlinks with a logger.warning rather than
silently dereferencing them. `Path.is_file()` returns True for
symlinks-to-files, and the original `read_bytes()` would have copied
the link target's bytes (e.g. `/etc/passwd`) into the project. The
helper's contract is "byte-for-byte tree mirror", which symlink
follow-through violated.
UX / API
- `copy_tree_atomic` gains `mode=0o644` parameter (was inheriting
`atomic_write_bytes`'s 0o600 default). Skill content is meant to be
read by other tools (Claude Code, Gemini CLI, Codex) so 0o600
propagation would have made fan-out scripts unreadable on multi-user
systems. `Lockfile` keeps the 0o600 default for state files via its
own `atomic_write_bytes` call.
- `_install_asset` raises `FileNotFoundError` when `project_root` does
not exist instead of silently scaffolding `.memtomem/` inside a
fresh directory. A typo'd `/Users/me/projetc` errors loudly now.
- `COPY_SKIP_NAMES = {".git", ".DS_Store", "__pycache__"}` extends the
skip list — Finder side-effects on macOS and Python bytecode caches
in wikis no longer leak into `<project>/.memtomem/`.
Type tightening
- `Lockfile.__init__(path: Path | str)` annotation widens to match the
runtime `Path(path).expanduser()` body.
- `InstallResult.asset_type: Literal["skills"]` for the public dataclass
in PR-B; `_install_asset` keeps a wider parameter type so PR-C's
agent/command widening stays a one-line tweak. Construction site uses
`cast` with a comment explaining the boundary.
Concurrency contract
- `_install_asset` docstring documents same-asset race semantics
(last-write-wins on identical bytes) so reviewers don't confuse it
with the distinct-asset concurrency the existing test exercises.
Explicit same-asset test deferred — the design contract is now in
the docstring and PR-C will exercise it via widening.
Tests
- 4 new tests: `test_install_skips_dsstore_and_pycache`,
`test_install_skips_symlinks_in_source`,
`test_install_files_written_with_default_mode`,
`test_install_project_root_missing`.
- All pre-existing tests updated to use `tmp_path` directly as project
root (always exists), keeping the new is_dir() check happy.
Skipped (per review reasoning)
- Same-skill concurrent-install test (covered by docstring; PR-C will
exercise widening).
- `os.fspath()` for cross-platform CLI path display (no cross-platform
test surface to validate).
Verification
- `uv run ruff check` + `uv run ruff format --check` on every changed file: clean.
- `uv run pytest tests/test_lockfile.py tests/test_context_install.py
tests/test_wiki_*.py tests/test_context_projects.py
tests/test_context_skills.py`: 114 passed (PR-B + PR-A regression +
projects/skills regression).
- `uv run mypy` advisory on the three modified source files: 0 issues.
Co-Authored-By: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements ADR-0008 PR-B: install a wiki asset into a project tree with a recorded commit pin and concurrency-safe lockfile. Builds on PR-A (#622) which scaffolded
~/.memtomem-wiki/,mm wiki init, andmm wiki list.mm context install skill <name>— copytree snapshot of~/.memtomem-wiki/skills/<name>/into<project>/.memtomem/skills/<name>/, pinned to the wiki's HEAD commit.<project>/.memtomem/lock.json(version 1) recordswiki_commit(full 40-char SHA) and microsecond-precisioninstalled_at. Reads round-trip throughdictso unknown forward-compat fields survive untouched (ADR-0008 §Lockfile schema).git pullcannot drift the recordedwiki_commitfrom the bytes that were copied.Scope
PR-B is skill-only by design. Agents and commands land in PR-C alongside override resolution — without override-aware extraction the canonical snapshot exists on disk but does not flow through fan-out, which would surprise users into thinking install is broken. The
Choice(['skill'])widens toChoice(['skill', 'agent', 'command'])in one line when PR-C lands; the internal_install_asset(asset_type, …)already takes the wider parameter.Out of scope per ADR-0008 PR breakdown:
OVERRIDE_FORMATS,mm wiki <type> overridemm context update,--force,.bak,mm context status,install --all, mtime/dirty detection--commit <ref>flag, directory-levelos.replaceatomic snapshotRefusal posture (forward-protects Invariant 2)
AlreadyInstalledErrorfires on either existing-lockfile-entry OR existing-dest-dir, not AND. Catches the four cases:The error message classifies which side is present so the user knows exactly what to remove.
Refactors carried in this PR
copy_tree_atomichoisted fromcontext/skills.pytocontext/_atomic.pyas a public helper with defensive.gitskip (single-file ripple verified bygit grep)._file_lock+_lock_path_forhoisted fromcontext/projects.pytocontext/_atomic.pysoKnownProjectsStoreandLockfileshare the PR-feat(index): mm index --debounce-window + --flush + --status (closes #536 documented gap) #548 sidecar pattern instead of cross-importing private symbols.git_identity/wiki_rootfixtures moved totests/_wiki_fixtures.pyand re-exported fromconftest.py—test_wiki_store.py,test_wiki_cmd.py, andtest_context_install.pyshare one definition.Decisions worth flagging for review
dict[str, Any]version != 1handlingLockfileVersionErrorin strict (write) paths;load(strict=False)returns raw dictmm context status(PR-D) needs to render unknown versions diagnostically without forking the loadersource_pathlockfile field(asset_type, name); ADR schema doesn't include itinstalled_atprecisionUser-facing docs (README, getting-started) for
mm context installdeferred until the wiki series is feature-complete after PR-D — matches how PR-A shipped ADR-only.Test plan
uv run pytest tests/test_lockfile.py tests/test_context_install.py— 26 new tests passuv run pytest tests/test_wiki_store.py tests/test_wiki_cmd.py— 28 PR-A regression tests still pass after fixture refactoruv run pytest tests/test_context_projects.py tests/test_context_skills.py— 56 regression tests still pass after_file_lockandcopy_tree_atomichoistsuv run pytest -m "not ollama"— full suite, only the pre-existingtest_calibrate_portfoliolive-corpus check fails (unrelated; same on origin/main)uv run ruff check+uv run ruff format --checkon every changed file — cleanuv run mypy(advisory) on the four new/modified source files — 0 issuesHOME=/tmp/...:mm wiki init→ seed skill →mm context install skill hello(succeeds, lock.json + dest tree correct) → re-run (refuses with classified error)🤖 Generated with Claude Code