Self-managing setup: wiring manifest, doctor, session autocheck - #127
Merged
Conversation
Approach A: one declarative wiring manifest drives install, drift detection, and auto-repair across ~/.claude/settings.json, ~/.claude.json, CLAUDE.md managed block, skills, and per-repo post-commit hooks. Windows-first installer, ollama removed from setup path, backend-agnostic wiring, ralph local covered by inheritance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Repo-move heal via one doctor command, core.hooksPath-aware post-commit install, uv bootstrap in setup.ps1; verified-safe and minor/accepted assumption buckets carried into the spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten TDD tasks: manifest, three new hook modules, engine (render/inspect/diff/apply), golden parity fixture, per-repo post-commit installer, CLI setup+doctor, bootstrap autocheck with kill switch, bootstrap scripts, docs sweep. Per-task confidence 90-95% with embedded mitigations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pt gaps Task 8: never cache the wiring fingerprint when apply() returns warnings, otherwise persistent drift is reported once then silenced. Task 5: repo is public, so parity fixtures are trimmed to the managed subset and username-sanitized. Task 4: apply() takes home and creates backup/lock dirs itself. Task 9: setup.ps1 cds to the repo root. Confidence now 92-95% across all tasks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eError
pre_compact.py and commit_checkpoint.py did not validate that parsed JSON was
a dict before calling .get() methods. Valid-but-non-dict JSON (null, [], 42)
raised uncaught AttributeError and violated "hooks never raise" constraint.
Also added isinstance() check in commit_checkpoint for tool_input not being a
dict, preventing {"tool_input": null} from raising on second .get() call.
Changes:
- pre_compact.py: coerce non-dict parsed JSON to {}
- commit_checkpoint.py: coerce non-dict parsed JSON and tool_input to {}
- test_pre_compact.py: added tests for null and array JSON
- test_commit_checkpoint.py: added tests for null, array, and null tool_input
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port merge_settings_json and merge_claude_json from cli/install_hooks.py into better_memory/setup/engine.py, generalized to all 8 MANAGED_HOOKS specs (incl. if_filter groups) and extended with env merge. Adds render/patch_mcp_entry/splice_managed_block/extract_managed_block/ diff/fingerprint — the pure render/inspect/diff half of the self-managing setup engine; apply() (I/O) lands in a later task. Adds a narrowly-scoped ruff per-file-ignore (F401) for tests/setup/test_engine.py: the brief-mandated verbatim test imports render for API-surface documentation but exercises it only indirectly via fingerprint(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review findings on engine.py: 1. diff() raised TypeError on valid-but-non-dict settings.json/.claude.json content (5, null, [1,2,3], "text") because merge_settings/patch_mcp_entry both do dict(existing). _load_json_or_drift now validates isinstance(..., dict) and treats non-dict JSON the same as malformed JSON: one drift line, no exception. 2. render() and merge_settings()'s ADD pass built the same per-event matcher-group structure twice. Factored into _canonical_hook_groups(), called from both, so they cannot drift apart from each other. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the I/O half of better_memory/setup/engine.py: apply() writes the managed subset of settings.json, .claude.json, CLAUDE.md, and skill symlinks, backing up any existing file before each write. Serializes concurrent apply() calls via a lock file (60s stale timeout) and retries once if ~/.claude.json is rewritten by Claude Code itself between the pre-write read and the atomic write (spec concern 3). Malformed JSON in a target file produces a warning and leaves that file untouched. _backup/_atomic_write are duplicated from cli/install_hooks.py rather than imported, since that module becomes a shim over this engine in a later task and must not become a dependency of it. install_skills() ports install_skill_symlinks, parameterized by target dir + repo root and extended to loop MANAGED_SKILLS. Also vendors the start-better-memory-ui skill into .claude/skills/ (it previously lived only in ~/.claude/skills/) and rewrites its machine-absolute paths to repo-relative wording so it can serve as the symlink source alongside the other two managed skills. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolve hooks directory via git rev-parse --git-path for worktrees, which store .git as a file (gitdir:) not a directory. Maintains core.hooksPath precedence and handles bare repos (resolve to None). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds `better-memory setup`/`doctor` subcommands (better_memory/cli/setup_cmd.py) wired into the main CLI dispatcher, and guts cli/install_hooks.py into a thin deprecation shim that delegates to the setup engine (Tasks 3-4 already ported its merge/backup/symlink logic there). install_hooks fallout: deleted unit tests exercising removed internals (tests/cli/test_install_hooks.py, test_install_skill_symlink.py) and the e2e subprocess suite for the old flag-driven CLI contract (tests/e2e/test_install_hooks.py) - their merge-logic coverage already lives in tests/setup/test_engine*.py. Ported the previously-uncovered symlink replacement-ladder/OSError-fallback/junction-recognition scenarios into tests/setup/test_engine_apply.py against engine.install_skills() directly. tests/e2e/test_setup_sh.py is skip-marked (not deleted): scripts/setup.sh is unchanged here and still drives the old install_hooks flag contract until a later task migrates it to call `better-memory setup` directly. tests/e2e_meta/test_canary_home.py's REDUCED_DEFAULT scope updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… migration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… skills Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…steps README/website/hooks-setup now describe the two bootstrap scripts, the setup/doctor CLI (flags copied from cli/setup_cmd.py's argparse), the BETTER_MEMORY_WIRING_AUTOCHECK env var, the new state/install-backups layout, and the automatic per-repo post-commit install. Removes the retired CLAUDE.md drift sentinel and the manual-paste/install_hooks framing; corrects a pre-existing async/pythonw mismatch in the hook registration reference JSON. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The manual-setup pythonw.exe generalization was removed in the prior commit but survived in Troubleshooting's "Windows console flashes" entry, which would break stdout-dependent hooks (all but observer) if followed literally. Scopes the advice to observer only and points at `better-memory doctor --fix` to restore correct interpreter assignments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…it hooks, record autocheck failures, guard junction fallback Final-review fix wave for feat/self-managing-setup: - cli/setup_cmd.py: handle_setup/handle_doctor now resolve home via _common.resolve_home() before calling detect_machine_params(), fixing a split-brain where setup/doctor wrote wiring under the real home while hooks operated against a custom BETTER_MEMORY_HOME. - setup/repo_hook.py: ensure_post_commit now re-points the managed invocation line when it drifts from the current machine params (e.g. after a repo/venv move), instead of treating any sentinel match as permanently installed and silently invoking a dead interpreter forever. - hooks/session_bootstrap.py: a failing wiring autocheck is now recorded via record_hook_error(hook_name="wiring_autocheck", ...) instead of vanishing into a bare except-pass, so persistent drift failures leave a diagnostic trail. - setup/engine.py: the Windows junction-fallback subprocess.run call is now guarded against SubprocessError/OSError (e.g. TimeoutExpired) so it degrades to a warning instead of crashing apply(). - docs/hooks-setup.md: moved the commit_checkpoint example's "if" key from the matcher group onto the hook entry, matching what manifest.py::hook_entry actually renders. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔴 Claude BugBot Analysis
Found 1 potential bug in this PR.
medium: 1
One confirmed bug: the session-level wiring autocheck's mtime+fingerprint cache omits the skills directory, so it silently fails to detect and repair a deleted/broken skill symlink after the first successful run, even though the full engine.diff() check (used by better-memory doctor) correctly flags it.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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
better-memory now owns its entire Claude Code wiring. One declarative manifest drives install, drift detection, and automatic repair — no manual setup steps remain.
better_memory/setup/:manifest.py(declarative managed surface: 8 hooks, env, MCP entry, CLAUDE.md managed block, 3 skills),engine.py(render/inspect/diff/apply with timestamped backups, atomic writes, apply lock, re-read+retry on~/.claude.json, legacy-section absorption/dedup for CLAUDE.md, Windows junction fallback for skill links),autocheck.py(session-start drift check with mtime+fingerprint cache, warnings-never-cached rule,BETTER_MEMORY_WIRING_AUTOCHECKkill switch),repo_hook.py(worktree-safe per-repo post-commit installer honoringcore.hooksPath, self-re-pointing after repo moves).pre_compact,commit_checkpoint,stop_sweepreplace the loose script and echo hooks.better-memory setupandbetter-memory doctor [--fix] [--json];install_hooksreduced to a deprecation shim.scripts/setup.ps1(installs uv if missing),scripts/setup.shslimmed to three steps; Ollama removed from the setup path.Live migration executed and verified on the reference machine:
doctorreports wiring clean; duplicated CLAUDE.md healed (224 -> 121 lines).Test plan
Follow-ups (tracked, not in this PR)
better-memory setupunder an isolated HOME; rewrite or delete the skip-markedtest_setup_sh.py.tests/hooks/test_post_commit.py:169.🤖 Generated with Claude Code