From 2a788b741de6c314998c04cf6e990fa10255f673 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 10 Nov 2026 14:25:05 +0100 Subject: [PATCH] Repoint MCP paths; parallelise mutation gate The mutation hook alone took 51 s, so no commit touching the session_autopsy test could fit the 60 s cap. Mutants now run 4 wide (19 s). Two oldest evidence files pruned for the 60-file cap. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01WzxdWYFhDShgeHByh6yT9L --- .../book-guard-bar-block-2026-10-02.json | 49 ------------------- .../mcp-servers-path-repoint-2026-11-10.json | 34 +++++++++++++ ...-session-autopsy-mutations-2026-11-10.json | 31 ++++++++++++ .../remove-wsg-grabber-2026-09-28.json | 37 -------------- linux_configuration/scripts/blender-mcp-host | 4 +- linux_configuration/scripts/blender-mcp-serve | 2 +- .../tests/test_session_autopsy_hooks.py | 10 ++-- meta/scripts/mutate_session_autopsy_hooks.py | 28 +++++++---- 8 files changed, 92 insertions(+), 103 deletions(-) delete mode 100644 docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json create mode 100644 docs/superpowers/evidence/mcp-servers-path-repoint-2026-11-10.json create mode 100644 docs/superpowers/evidence/parallel-session-autopsy-mutations-2026-11-10.json delete mode 100644 docs/superpowers/evidence/remove-wsg-grabber-2026-09-28.json diff --git a/docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json b/docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json deleted file mode 100644 index f141e4d8..00000000 --- a/docs/superpowers/evidence/book-guard-bar-block-2026-10-02.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "intent": "Make book-guard openable from the i3 bar the same way screen-locker is: a [book_guard] block shows this month's paper-book pace, and a click opens the book-guard desktop window. Move the shared pins to gatelock 0.9.0 (the new shared morning-session reader) and today's newest stable releases.", - "scope": [ - "linux_configuration/i3blocks/book_guard.sh - new block script", - "linux_configuration/i3blocks/config - [book_guard] entry next to [screen_locker]", - "linux_configuration/tests/test_i3blocks_book_guard.sh - behaviour test", - "meta/requirements.txt - gatelock 0.9.0 and newest-stable pins", - "Non-goal: the stale tracked linux_configuration/i3/config (it already lacks the workout-locker and leetcode-guard exec lines); the live ~/.config/i3/config got the book-guard exec line." - ], - "changes": [ - "book_guard.sh reads the state.json book-guard publishes into ~/data/cloud/Reading on every inbox pass and gate run (no server, one jq pass): green on pace, yellow behind but unlocked (escaped / free day / unlocked tag), red when locked, orange when missing or older than 45 min, red with DISARMED when book-guard.timer is inactive. A click runs book-guard-desktop via setsid, which opens another window if it is already serving.", - "meta/requirements.txt: gatelock 0.8.2 -> 0.9.0; anthropic 1.11.0, cryptography 50.0.2, flake8-bugbear 26.9.30, geopandas 1.2.0, howlongtobeatpy 1.0.24, pyflakes 4.0.1, selenium 4.50.0, mypy 2.4.0, pylint 4.1.1, ruff 0.16.10." - ], - "verification": [ - { - "command": "bash linux_configuration/tests/test_i3blocks_book_guard.sh", - "result": "pass", - "evidence": "on pace / locked / escaped / free day / other / stale / disarmed / missing / broken / click all asserted; systemctl and the launcher stubbed on PATH." - }, - { - "command": "bash linux_configuration/tests/test_i3blocks_efficiency.sh", - "result": "pass", - "evidence": "All i3blocks efficiency regression tests passed." - }, - { - "command": "~/.config/i3blocks/book_guard.sh (live state.json)", - "result": "pass", - "evidence": "'๐Ÿ“– 0/300 ยท 10p behind (escaped)' #F1FA8C; block live in the bar after i3-msg reload." - }, - { - "command": ".venv/bin/python -m pytest python_pkg/code_tutor (the only anthropic/geopandas/selenium/cryptography importer)", - "result": "pass", - "evidence": "217 passed on anthropic 1.11.0." - }, - { - "command": "pre-commit run --files ", - "result": "pass", - "evidence": "all hooks passed, including dependency-freshness." - } - ], - "risks": [ - "cryptography 50.0.2 stays outside pyopenssl/mitmproxy's declared ranges, as 50.0.1 already was.", - "A click with book-guard-desktop missing does nothing (errors are silenced so the bar never breaks)." - ], - "rollback": [ - "git revert this commit and remove ~/.config/i3blocks/book_guard.sh; i3-msg reload.", - "Confirm the bar still renders [screen_locker] and the other blocks." - ] -} diff --git a/docs/superpowers/evidence/mcp-servers-path-repoint-2026-11-10.json b/docs/superpowers/evidence/mcp-servers-path-repoint-2026-11-10.json new file mode 100644 index 00000000..0aecde24 --- /dev/null +++ b/docs/superpowers/evidence/mcp-servers-path-repoint-2026-11-10.json @@ -0,0 +1,34 @@ +{ + "intent": "blender-mcp-host/serve and the session_autopsy hook test point at the relocated MCP server checkouts (~/.local/share/mcp-servers, ~/.claude/mcp-servers) instead of the removed ~/src/mcp-servers.", + "scope": [ + "linux_configuration/scripts/blender-mcp-host", + "linux_configuration/scripts/blender-mcp-serve", + "linux_configuration/tests/test_session_autopsy_hooks.py", + "Non-goal: moving or re-cloning the MCP servers themselves" + ], + "changes": [ + "blender-mcp-host: ADDON_PATH and the embedded start_addon spec path now /home/kuhy/.local/share/mcp-servers/blender-mcp/addon.py", + "blender-mcp-serve: SERVER_DIR now /home/kuhy/.local/share/mcp-servers/blender-mcp", + "test_session_autopsy_hooks.py: expected PYTHONPATH is {home}/.claude/mcp-servers/session-autopsy; comment names kuhyx/claude-config" + ], + "verification": [ + { + "command": "python -m pytest linux_configuration/tests/test_session_autopsy_hooks.py -q -p no:randomly --no-cov", + "result": "pass", + "evidence": "12 passed in 4.12s" + }, + { + "command": "grep -c src/mcp-servers linux_configuration/scripts/blender-mcp-host linux_configuration/scripts/blender-mcp-serve linux_configuration/tests/test_session_autopsy_hooks.py", + "result": "pass", + "evidence": "0 matches in each of the three files" + } + ], + "risks": [ + "Hardcoded absolute /home/kuhy paths still break on another account or machine", + "If the checkout under ~/.local/share/mcp-servers is missing, blender-mcp-serve fails at start" + ], + "rollback": [ + "git revert the commit; paths return to ~/src/mcp-servers", + "Re-run test_session_autopsy_hooks.py and start blender-mcp-serve once to confirm" + ] +} diff --git a/docs/superpowers/evidence/parallel-session-autopsy-mutations-2026-11-10.json b/docs/superpowers/evidence/parallel-session-autopsy-mutations-2026-11-10.json new file mode 100644 index 00000000..fbfa4677 --- /dev/null +++ b/docs/superpowers/evidence/parallel-session-autopsy-mutations-2026-11-10.json @@ -0,0 +1,31 @@ +{ + "intent": "The session_autopsy mutation gate finishes in about 19 s instead of about 51 s so a commit touching it fits the 60 s per-action cap.", + "scope": [ + "meta/scripts/mutate_session_autopsy_hooks.py", + "Non-goal: changing the mutation definitions or the pass/fail rule" + ], + "changes": [ + "Mutants run in a ThreadPoolExecutor (4 workers) via _run_mutant; each uses its own hook copy and its own coverage file", + "Per-mutant report lines are still printed in MUTATIONS order after all finish" + ], + "verification": [ + { + "command": "python3 meta/scripts/mutate_session_autopsy_hooks.py (before)", + "result": "pass", + "evidence": "PASS: all 12 tests killed, 51.4 s wall" + }, + { + "command": "python3 meta/scripts/mutate_session_autopsy_hooks.py (after)", + "result": "pass", + "evidence": "PASS: all 12 tests killed, same per-mutant kill lists, 19.1 s wall" + } + ], + "risks": [ + "Four concurrent pytest runs use more CPU and RAM for a few seconds", + "A test that shares global state across processes could become flaky under concurrency" + ], + "rollback": [ + "git revert the commit; mutants run sequentially again", + "Re-run the script and expect PASS in about 51 s" + ] +} diff --git a/docs/superpowers/evidence/remove-wsg-grabber-2026-09-28.json b/docs/superpowers/evidence/remove-wsg-grabber-2026-09-28.json deleted file mode 100644 index 657367f1..00000000 --- a/docs/superpowers/evidence/remove-wsg-grabber-2026-09-28.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "intent": "Remove python_pkg/wsg_grabber from the monorepo now that it lives in its own private repo, kuhyx/wsg-grabber, with its full history; and turn main's CI green again (hypothesis released 6.168.3).", - "scope": [ - "python_pkg/wsg_grabber/ deleted (package, tests, README, install.sh)", - "AGENTS.md (CLAUDE.md symlink): wsg-grabber added to the extracted-repos list", - "meta/requirements.txt: hypothesis 6.168.2 -> 6.168.3, the only item the Pre-commit checks and dependency freshness jobs reported behind on 33f0fee3", - "Non-goal: the historical mentions in docs/DOCS-python-split-recipes.md and docs/DOCS-restructure-live-state.md stay; they record what happened here" - ], - "changes": [ - "git filter-repo kept the 14 commits that touched python_pkg/wsg_grabber; the standalone setup is kuhyx/wsg-grabber 6b6d4b3", - "/usr/local/bin/wsg-grabber regenerated by ~/src/wsg-grabber/install.sh, so the launcher no longer depends on this repo" - ], - "verification": [ - { - "command": "~/src/wsg-grabber: .venv/bin/python -m pytest -p no:cacheprovider -q", - "result": "pass", - "evidence": "303 passed, TOTAL 1645 stmts / 342 branches, 100.00% coverage; pre-commit run --all-files clean there" - }, - { - "command": "vm run wsg 'sudo bash ~/install.sh' then on the host: sudo bash ~/src/wsg-grabber/install.sh && wsg-grabber stats", - "result": "pass", - "evidence": "vmbox: packages installed, launcher written with PYTHONPATH=/home/arch, --uninstall removed it. Host: launcher PYTHONPATH=/home/kuhy/src/wsg-grabber; stats reads the live index (26898 known, 706 kept, 200 passed, 25992 purged)" - }, - { - "command": "gh run view 36399404250 --log-failed", - "result": "pass", - "evidence": "the only freshness finding was pypi:hypothesis 6.168.2 -> 6.168.3 at meta/requirements.txt:42, bumped here" - } - ], - "risks": [ - "Anything outside git that pointed at ~/src/testsAndMisc/python_pkg/wsg_grabber would break; the only such artefact found was /usr/local/bin/wsg-grabber, already repointed." - ], - "rollback": [ - "git revert this commit to restore the package, then rerun its install.sh to point the launcher back here", - "Validate with `wsg-grabber stats`" - ] -} diff --git a/linux_configuration/scripts/blender-mcp-host b/linux_configuration/scripts/blender-mcp-host index e8e5f59a..605187a6 100755 --- a/linux_configuration/scripts/blender-mcp-host +++ b/linux_configuration/scripts/blender-mcp-host @@ -20,7 +20,7 @@ set -euo pipefail SCRIPT_NAME="$(basename "$0")" readonly SCRIPT_NAME -readonly ADDON_PATH="/home/kuhy/src/mcp-servers/servers/blender-mcp/addon.py" +readonly ADDON_PATH="/home/kuhy/.local/share/mcp-servers/blender-mcp/addon.py" readonly START_SCRIPT="/home/kuhy/.local/share/blender-mcp-host/start_addon.py" readonly LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/blender-mcp-host.log" @@ -87,7 +87,7 @@ import importlib.util import sys _SPEC = importlib.util.spec_from_file_location( - "blendermcp_addon", "/home/kuhy/src/mcp-servers/servers/blender-mcp/addon.py") + "blendermcp_addon", "/home/kuhy/.local/share/mcp-servers/blender-mcp/addon.py") _MOD = importlib.util.module_from_spec(_SPEC) sys.modules["blendermcp_addon"] = _MOD _SPEC.loader.exec_module(_MOD) diff --git a/linux_configuration/scripts/blender-mcp-serve b/linux_configuration/scripts/blender-mcp-serve index b40337d5..441e48c6 100755 --- a/linux_configuration/scripts/blender-mcp-serve +++ b/linux_configuration/scripts/blender-mcp-serve @@ -14,7 +14,7 @@ set -euo pipefail readonly HOST_SCRIPT="/home/kuhy/.local/bin/blender-mcp-host" -readonly SERVER_DIR="/home/kuhy/src/mcp-servers/servers/blender-mcp" +readonly SERVER_DIR="/home/kuhy/.local/share/mcp-servers/blender-mcp" readonly UV="/home/kuhy/.local/bin/uv" # stdout is the MCP JSON-RPC channel -- any stray byte written there corrupts diff --git a/linux_configuration/tests/test_session_autopsy_hooks.py b/linux_configuration/tests/test_session_autopsy_hooks.py index b8c452da..fbe3d6fe 100644 --- a/linux_configuration/tests/test_session_autopsy_hooks.py +++ b/linux_configuration/tests/test_session_autopsy_hooks.py @@ -144,16 +144,16 @@ class TestSessionEndHookLaunchesIngest: json.dumps({"transcript_path": str(transcript)}), ) argv = _await_launch(record) - # session_autopsy was extracted from this monorepo to kuhyx/mcp-servers; - # the hook must launch it from there, not from a python_pkg that no longer - # exists here. This test reads the DEPLOYED hook, which is why it caught the - # move rather than passing against a stale copy. + # session_autopsy was extracted from this monorepo to kuhyx/claude-config + # (~/.claude/mcp-servers); the hook must launch it from there, not from a + # python_pkg that no longer exists here. This test reads the DEPLOYED hook, + # which is why it caught the move rather than passing against a stale copy. assert "-m session_autopsy" in argv assert "python_pkg" not in argv assert "ingest" in argv assert str(transcript) in argv assert "--quiet" in argv - assert f"PYTHONPATH={home}/src/mcp-servers/session-autopsy" in argv + assert f"PYTHONPATH={home}/.claude/mcp-servers/session-autopsy" in argv assert "timeout 120" in argv def test_creates_the_autopsy_home(self, tmp_path: Path) -> None: diff --git a/meta/scripts/mutate_session_autopsy_hooks.py b/meta/scripts/mutate_session_autopsy_hooks.py index 8fdc8f92..92c147ba 100755 --- a/meta/scripts/mutate_session_autopsy_hooks.py +++ b/meta/scripts/mutate_session_autopsy_hooks.py @@ -25,6 +25,7 @@ nothing to mutate. from __future__ import annotations import ast +import concurrent.futures import os from pathlib import Path import re @@ -36,6 +37,9 @@ import tempfile from _session_autopsy_mutations import END_HOOK, MUTATIONS, START_HOOK, Mutation _TEST_FILE = "linux_configuration/tests/test_session_autopsy_hooks.py" +# Mutants are independent (own hook copy, own coverage file), so they run side by +# side; sequentially the ~4 s suite x 9 mutants pushed a commit past 60 s. +_WORKERS = 4 _OUTCOME = re.compile(r"::(test_\w+)\s+(PASSED|FAILED|ERROR|SKIPPED)") @@ -133,6 +137,14 @@ def _apply(mutation: Mutation, hooks_dir: Path, target_dir: Path) -> None: target.chmod(0o755) +def _run_mutant(mutation: Mutation, hooks_dir: Path) -> tuple[Mutation, set[str]]: + """Apply one mutation in a private directory and return the tests it kills.""" + with tempfile.TemporaryDirectory() as mutant_name: + mutant_dir = Path(mutant_name) + _apply(mutation, hooks_dir, mutant_dir) + return mutation, _run_suite(mutant_dir, mutant_dir) + + def main() -> int: """Run every mutation and fail if any test survives them all.""" hooks_dir = _hooks_dir() @@ -148,15 +160,13 @@ def main() -> int: return 1 killed_by: dict[str, list[str]] = {} - for mutation in MUTATIONS: - try: - with tempfile.TemporaryDirectory() as mutant_name: - mutant_dir = Path(mutant_name) - _apply(mutation, hooks_dir, mutant_dir) - failures = _run_suite(mutant_dir, scratch) - except RuntimeError as error: - _emit(f"{mutation.name:26s} ERROR: {error}") - return 1 + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=_WORKERS) as pool: + results = list(pool.map(lambda m: _run_mutant(m, hooks_dir), MUTATIONS)) + except RuntimeError as error: + _emit(f"ERROR: {error}") + return 1 + for mutation, failures in results: _emit(f"{mutation.name:26s} killed {len(failures):2d}: {sorted(failures)}") for test in failures: killed_by.setdefault(test, []).append(mutation.name)