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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWYFhDShgeHByh6yT9L
This commit is contained in:
Krzysztof Rudnicki 2026-11-10 14:25:05 +01:00
parent 1ee9035918
commit 2a788b741d
8 changed files with 92 additions and 103 deletions

View File

@ -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 <changed-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."
]
}

View File

@ -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"
]
}

View File

@ -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"
]
}

View File

@ -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`"
]
}

View File

@ -20,7 +20,7 @@ set -euo pipefail
SCRIPT_NAME="$(basename "$0")" SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME 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 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" readonly LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/blender-mcp-host.log"
@ -87,7 +87,7 @@ import importlib.util
import sys import sys
_SPEC = importlib.util.spec_from_file_location( _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) _MOD = importlib.util.module_from_spec(_SPEC)
sys.modules["blendermcp_addon"] = _MOD sys.modules["blendermcp_addon"] = _MOD
_SPEC.loader.exec_module(_MOD) _SPEC.loader.exec_module(_MOD)

View File

@ -14,7 +14,7 @@
set -euo pipefail set -euo pipefail
readonly HOST_SCRIPT="/home/kuhy/.local/bin/blender-mcp-host" 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" readonly UV="/home/kuhy/.local/bin/uv"
# stdout is the MCP JSON-RPC channel -- any stray byte written there corrupts # stdout is the MCP JSON-RPC channel -- any stray byte written there corrupts

View File

@ -144,16 +144,16 @@ class TestSessionEndHookLaunchesIngest:
json.dumps({"transcript_path": str(transcript)}), json.dumps({"transcript_path": str(transcript)}),
) )
argv = _await_launch(record) argv = _await_launch(record)
# session_autopsy was extracted from this monorepo to kuhyx/mcp-servers; # session_autopsy was extracted from this monorepo to kuhyx/claude-config
# the hook must launch it from there, not from a python_pkg that no longer # (~/.claude/mcp-servers); the hook must launch it from there, not from a
# exists here. This test reads the DEPLOYED hook, which is why it caught the # python_pkg that no longer exists here. This test reads the DEPLOYED hook,
# move rather than passing against a stale copy. # which is why it caught the move rather than passing against a stale copy.
assert "-m session_autopsy" in argv assert "-m session_autopsy" in argv
assert "python_pkg" not in argv assert "python_pkg" not in argv
assert "ingest" in argv assert "ingest" in argv
assert str(transcript) in argv assert str(transcript) in argv
assert "--quiet" 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 assert "timeout 120" in argv
def test_creates_the_autopsy_home(self, tmp_path: Path) -> None: def test_creates_the_autopsy_home(self, tmp_path: Path) -> None:

View File

@ -25,6 +25,7 @@ nothing to mutate.
from __future__ import annotations from __future__ import annotations
import ast import ast
import concurrent.futures
import os import os
from pathlib import Path from pathlib import Path
import re import re
@ -36,6 +37,9 @@ import tempfile
from _session_autopsy_mutations import END_HOOK, MUTATIONS, START_HOOK, Mutation from _session_autopsy_mutations import END_HOOK, MUTATIONS, START_HOOK, Mutation
_TEST_FILE = "linux_configuration/tests/test_session_autopsy_hooks.py" _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)") _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) 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: def main() -> int:
"""Run every mutation and fail if any test survives them all.""" """Run every mutation and fail if any test survives them all."""
hooks_dir = _hooks_dir() hooks_dir = _hooks_dir()
@ -148,15 +160,13 @@ def main() -> int:
return 1 return 1
killed_by: dict[str, list[str]] = {} killed_by: dict[str, list[str]] = {}
for mutation in MUTATIONS: try:
try: with concurrent.futures.ThreadPoolExecutor(max_workers=_WORKERS) as pool:
with tempfile.TemporaryDirectory() as mutant_name: results = list(pool.map(lambda m: _run_mutant(m, hooks_dir), MUTATIONS))
mutant_dir = Path(mutant_name) except RuntimeError as error:
_apply(mutation, hooks_dir, mutant_dir) _emit(f"ERROR: {error}")
failures = _run_suite(mutant_dir, scratch) return 1
except RuntimeError as error: for mutation, failures in results:
_emit(f"{mutation.name:26s} ERROR: {error}")
return 1
_emit(f"{mutation.name:26s} killed {len(failures):2d}: {sorted(failures)}") _emit(f"{mutation.name:26s} killed {len(failures):2d}: {sorted(failures)}")
for test in failures: for test in failures:
killed_by.setdefault(test, []).append(mutation.name) killed_by.setdefault(test, []).append(mutation.name)