release(v4.9.5): a claude purge removes every path the session owns (Issue #31, PR #59) - archive the review reports, bump the lockstep version, load logroot eagerly

Review loop (--review-only, --all-reviewer, focused on this Mac:
macOS, /usr/bin/python3 3.9.6, bash 3.2): round 1 (@6f4f40a) agy and
grok PASS, claude and codex NOT PASS (a non-uuid id `memory` purged the
auto-memory dir, a linked dir below a root was followed, a NaN
startedAt counted as dead), sent as a PR comment and fixed by the
author in eleven commits; round 2 (@2a8d738) confirmed all three fixed
but 4/4 NOT PASS on one test holder (`sh -c "sleep 120" <uuid>` loses
the uuid on macOS /bin/sh), fixed in de6f3fc; round 3 (@de6f3fc) 4/4
PASS.

Full suite on macOS / Python 3.9.6 @de6f3fc: 1934 passed, 56 skipped,
1 error (the known #55 false positive: agy conversations of another
workspace). The real ~/.claude, /private/tmp/claude-<uid> and herdr
panes unchanged.

Also names lib_py.delegation.logroot in eager.CORE_MODULES (the v4.9.4
review's non-blocking note).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
GodopuandClaude Opus 5.5 committed 2026-10-18 01:25:15 +09:00
1 parent de6f3fc81f
commit 6fd417a7b7
32 files changed
+916 -26

No files matched your search

@@ -0,0 +1,51 @@
# Review 916abd41 — PR #59 second pass, Issue #31
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T135731Z-e93f0a/1` (`loop-20261017T135731Z-e93f0a`). Head `2a8d73825b3832376aeaf96b09548359dc3cb887`, tree `d146f1ac68bcc3485d24fb92edb87264e4d1e8cb`, base `26e2c43753169671b600d03274901dbe203988dd` is an ancestor. Versus base: 13 files, +1462/−30. Versus the first-review head `6f4f40a4adb2115d5997dffd31fb6473f05cab68`: 11 commits (`26fb1c4`..`2a8d738`), 7 files, +382/−50. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is still `26e2c43753169671b600d03274901dbe203988dd`, porcelain empty. No product file was edited.
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57 (and `/bin/sh` is that bash). `bash -n` accepts `stop_session.sh` and `lib.sh`.
Every purge and `in-use` exercised here used a temporary `HOME`, `CLAUDE_CONFIG_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CODE_TMPDIR`, and `TMPDIR`. The real `~/.claude` and `/private/tmp/claude-<uid>` were listed with read-only counts. `_session_file_live` was called on the real session files and deleted nothing.
## What this Mac already stores
`~/.claude` has real directories `file-history`, `session-env`, `sessions`, `jobs`, and `projects`. `file-history` holds 19 entries and no symlinks. `session-env` holds 21 entries and no symlinks. `sessions` holds 6 `*.json` files and 6 key files. `jobs` holds 3 directories, none of them symlinks, and 2 of those names are 8 characters. `projects` has 16 keys, none of them symlinks.
`/tmp` is a symlink to `/private/tmp`. `/tmp/claude-<uid>` and `/private/tmp/claude-<uid>` both exist, share one real path, and that directory has 7 entries. `CLAUDE_CODE_TMPDIR` was unset in this review process before the probes pointed it at a temp directory.
## lstart on this host
`ps -p <pid> -o lstart=` with `LC_ALL=C` prints five fields. With `TZ=UTC0` the string differs from the default-TZ string. `calendar.timegm` of the UTC string sits 0.367 s behind `time.time()` for a fresh `sleep`. `time.mktime` of the default-TZ string sits the same 0.367 s behind. macOS `ps` honors `TZ=UTC0`, so the UTC parse in `_session_file_live` (`claude.py:97-105`) is the process start, and a DST fold is the same instant in both readings.
On the six live session files, `_session_file_live` returned live for all six and stale for none. `lstart - startedAt/1000` is between −2.362 s and −0.771 s, so `lstart <= startedAt/1000 + 2` holds for every one of them.
A temp-home probe of the same function: a live pid with `startedAt` set to now, NaN, inf, `10**400`, a digit string, or boolean `True` is live. A dead pid is stale, including a dead pid whose `startedAt` is NaN (`ProcessLookupError` returns before the time check, `claude.py:78-81`). The same live pid with `startedAt` an hour earlier is stale.
## The three findings from the first review
Canonical uuid. `_canonical` (`claude.py:10-18`) returns the lowercase spelling only when the whole string is a uuid. `_canonical("memory")` is `None`, and an uppercase uuid comes back lowercase. On a temp config, `purge_artifacts("memory")` returned `[]`, residual was empty, `conversation_in_use("memory")` was false, and both `projects/<key>/memory/MEMORY.md` and `memory.jsonl` stayed. `purge_artifacts("....")` returned `[]`.
Linked directories. `_inside` (`claude.py:108-122`) walks `dirname(path)` with `lstat` and rejects a symlink component below the root, then requires the resolved parent to sit under the resolved root. A temp `<tmp-root>/<key>` symlink to an outside directory stayed a symlink, the outside `keep.txt` stayed, that linked path was listed by `residual_artifacts`, and the purged list had no outside path. The real `file-history/<uuid>` and the jsonl for that same id were removed once the holder was gone. A `file-history/<uuid>` symlink was unlinked and its target `secret.txt` stayed. A symlink child inside a real owned `<uuid>/` directory was removed with that directory, and the target file stayed.
Non-finite `startedAt`. The probe above: NaN, inf, and an int too large for float are live while the pid is alive, and a dead pid is stale. That is `claude.py:84-95`.
Uppercase spelling while a session file names a live pid: `conversation_in_use` of the uppercase id was true. `python -m lib_py.agents purge claude <UPPER> <ws>` exited 4 with `{"in_use": true, "purged": []}`, and `in-use` exited 4 with `What may hold it:` on stderr. The jsonl and `file-history/<uuid>` stayed (`__main__.py:146-148`).
A process that still has the lowercase uuid in its argv (`sh -c 'sleep 60; true' <uuid>`, argv0 `sh`, 57-character args, visible to `ps -eo pid=,ppid=,args=`) makes `conversation_in_use` true for the uppercase spelling and for the lowercase spelling (`claude.py:354`).
`purge_artifacts` itself has no in-use gate. It skips a live session file (`claude.py:323-327`) and still removes the other owned paths when called directly. The purge CLI and `stop_session.sh:290-298` both ask `conversation_in_use` first. Stop captures the in-use stderr with `2>&1 >/dev/null`, prints the records-preserved sentence, and exits 4. The passthrough `raise SystemExit(1)` is `stop_session.sh:327`. The stop SKILL.md exit-4 row (line 65) says the records are preserved and the session itself may already be down. `tests/data/skill_code_blocks.json` is unchanged since `6f4f40a` and still points that passthrough at `stop_session.sh:327`.
## Tests
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mam916-916abd41-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped. The suite's own fixtures then point `HOME` at the test tmp.
On the snapshot, 149 tests ran in 39.61 s: 2 failed, 146 passed, 1 skipped. Modules: `tests/test_issue31_purge_residual.py`, `tests/test_a4_adapter_contract.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`. The skip is `test_claude_skills_link_resolves_to_agents_skills` (`tests/test_issue42_skill_blocks.py:266`), because `.claude/skills` is absent in this worktree. The issue-31 cases for a malformed id including `memory`, a linked directory, a linked orphan the CLI must report, NaN/`10**400`/bool `startedAt`, the UTC lstart mock, `_SYSTEM_TMP` staying inside the test, and the stop exit-4 text all passed. So did the a4 composite purge and the issue-55 home guard.
Both failures are `test_any_spelling_of_the_uuid_is_the_lowercase_session` (upper and mixed) at `tests/test_issue31_purge_residual.py:318`. The asserts above that line passed: the uppercase id was in use through the session file, purge and in-use both exited 4, and the owned files stayed. Line 315 then starts `sh -c "sleep 120" <lowercase-uuid>`. Bash 3.2 replaces a one-command `-c` script with that command. `ps` shows argv0 `sleep` and an 8-character argument string, and the uuid is gone. `conversation_in_use` of the uppercase id is false, which is what line 318 asserts against. The `sleep 60; true` form keeps the uuid and the same call returns true. The check at `claude.py:354` sees a command line that still carries the lowercase id. The process this test starts has already dropped it.
The same new tests copied onto an archive of `6f4f40a` fail on the pre-fix behavior. That archive is a separate clone. One line, `_SYSTEM_TMP = "/tmp"`, was added there so the new autouse fixture could patch a name `6f4f40a` does not define. `_tmp_roots` in that commit still hardcodes `/tmp` and does not read the name. `purge_artifacts("memory")` returned a `session-env/memory` path (`test_issue31_purge_residual.py:285`). The linked-dir purge returned removed paths (`:351`). The CLI asked about a leftover behind a link did not report that residual (`:373`). NaN, a digit string, and `True` were stale for a live pid (`:539`), and the live-holder purge was allowed through (`:556`). An uppercase id was outside `conversation_in_use` (`:307`). 21 failed, 13 passed, in 2.98 s. The real `/tmp/claude-<uid>` entry count stayed 7.
The in-use gate in `stop_session.sh` still runs for every agent that has a purge uuid, agy included. That was already true at `6f4f40a`.
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `26e2c43`. The herdr config mtime is still 1784456394.
[VERDICT: NOT PASS]
@@ -0,0 +1,14 @@
{
"loop_id": "loop-20261017T145452Z-6c5aa6",
"base_sha": "26e2c43753169671b600d03274901dbe203988dd",
"head_sha": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
"tree_sha": "ffc291e6fc93822070f2b9f1d17009e83f176fcf",
"review_commit": "de6f3fc81f8c33a9d2335320742dc5a47f566cda",
"reviewer": "reviewer-grok-01",
"job_id": "b1d3a736",
"verdict": "PASS",
"transport": "herdr",
"evidence": "review-tree",
"isolation": "worktree",
"verified_at": "2026-10-17T15:16:54Z"
}
@@ -0,0 +1,33 @@
# Review b1d3a736 — PR #59 third pass, Issue #31
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261017T145452Z-6c5aa6/1` (`loop-20261017T145452Z-6c5aa6`). Head `de6f3fc81f8c33a9d2335320742dc5a47f566cda`, tree `ffc291e6fc93822070f2b9f1d17009e83f176fcf`, base `26e2c43753169671b600d03274901dbe203988dd` is an ancestor. Versus base: 25 commits. Versus the second-pass head `2a8d73825b3832376aeaf96b09548359dc3cb887` there is one commit, `de6f3fc`, and it changes one file: `tests/test_issue31_purge_residual.py`, +3/−1. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is still `26e2c43753169671b600d03274901dbe203988dd`, porcelain empty. No product file was edited.
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57. The second pass on `2a8d738` already measured the three Mac findings (canonical uuid, symlink boundary, non-finite `startedAt`) and the UTC `lstart` parse. This commit does not touch that product code. `conversation_in_use` still folds any spelling to the lowercase uuid and searches both that spelling and the one that was asked (`claude.py:350-354`).
## The holder keeps the uuid
`de6f3fc` replaces the stand-in at `tests/test_issue31_purge_residual.py:317`. The old line was `subprocess.Popen(["sh", "-c", "sleep 120", conv])`. macOS `/bin/sh` (bash 3.2) replaces a one-command `-c` script with `sleep`, and the uuid left the process list, so both spelling cases failed at the command-line assert. The new line is the same shape as `_holder` in `tests/test_issue10_13_purge.py:116`:
```python
named = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(120)", conv])
```
`sys` is the import already at the top of the file. The `try` / `finally` around it is unchanged: `named.kill()` and `named.wait()` are still `tests/test_issue31_purge_residual.py:321-323`.
A temp-home probe started that exact command with the control-plane interpreter (73-character path) and a fresh lowercase uuid. `ps -eo pid=,ppid=,args=`, the same invocation `conversation_in_use` uses, showed a 196-character argument string containing the uuid. The same string was complete with `COLUMNS=80` and with `COLUMNS=10000` (`COLUMNS` was unset in the probe process). The holder was still alive. `conversation_in_use` of the uppercase spelling and of the lowercase spelling were both true.
## Tests
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mamb1-b1d3a736-pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped.
On the snapshot, 149 tests ran in 42.05 s: 148 passed, 1 skipped, 1 error. Modules: `tests/test_issue31_purge_residual.py`, `tests/test_a4_adapter_contract.py`, `tests/test_issue42_skill_blocks.py`, `tests/test_issue55_agent_homes.py`. The skip is `test_claude_skills_link_resolves_to_agents_skills` (`tests/test_issue42_skill_blocks.py:266`), because `.claude/skills` is absent in this worktree. Both `test_any_spelling_of_the_uuid_is_the_lowercase_session` cases passed, along with the rest of the issue-31 file.
The error is the session-scoped issue-55 guard at teardown of `test_r1_adapter_discover_writes_nothing_into_a_panes_claude_project_dir` (`tests/conftest.py:1603`). It reported 14 new entries, all `*.db-shm` or `*.db-wal` sidecars for 7 ids under the agy conversations directory. The guard left them in place. This is the known false positive from an agy conversation in another workspace, and it is not a verdict basis.
Regression of the spelling fold. A separate clone of this snapshot had one edit: the command-line search in `conversation_in_use` looked up the asked spelling only, while session files still used the lowercase id. The session-file asserts and the CLI exit 4 still passed. Both spelling cases then failed at `tests/test_issue31_purge_residual.py:320` (`a command line naming the lowercase id`), in 1.62 s. The new holder is what makes that assert see a live lowercase argv, so dropping the lowercase search fails the test. The clone was discarded. The snapshot was not modified (porcelain still empty after that run).
The three non-blocking notes from the second pass are unchanged, because this commit is the test line only: a linked `projects/<key>` is reported as residual, a lowercase query does not see an uppercase argv, and `purge_artifacts` itself has no in-use gate. The purge CLI and `stop_session.sh` still ask `conversation_in_use` first.
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `26e2c43`. The herdr config mtime is still 1784456394.
[VERDICT: PASS]
@@ -0,0 +1,47 @@
# Review f0df1be0 — PR #59, Issue #31 claude purge
Reviewed only the snapshot `/var/folders/q_/68sxgw3j073dgjg8wl9s08vw0000gn/T/mam-review/loop-20261016T003524Z-a51548/1` (`loop-20261016T003524Z-a51548`). Head `6f4f40a4adb2115d5997dffd31fb6473f05cab68`, tree `f4d9422ad0c1d616b5f37065f3e96b34c28f78d8`, base `26e2c43753169671b600d03274901dbe203988dd` is an ancestor. Thirteen commits, 12 files, +1127/−27. `git diff --check` is clean. Snapshot porcelain is empty and the snapshot has no `.mam`. Control-plane main is still `26e2c43753169671b600d03274901dbe203988dd`, porcelain empty. No product file was edited.
Host tools: `/usr/bin/python3` 3.9.6, the control-plane venv the same 3.9.6, `/bin/bash` 3.2.57. `bash -n` accepts `stop_session.sh` and `lib.sh`. With `set -u`, the new `CLAUDE_PROJECT_DIR` default (`lib.sh:78`) expands to `$HOME/.claude/projects` when both variables are unset, and to `$CLAUDE_CONFIG_DIR/projects` when only the config dir is set.
Every purge, `in-use`, and `stop --purge-conversation` exercised here used a temporary `HOME`, `CLAUDE_CONFIG_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CODE_TMPDIR`, and `TMPDIR`. The real `~/.claude` and `/private/tmp/claude-<uid>` were listed with `ls`-level reads.
## What this Mac already stores
`~/.claude` has real directories `file-history`, `session-env`, `sessions`, `jobs`, and `projects`. `file-history` holds 18 uuid-named directories and no symlinks. `session-env` holds 21 uuid-named directories and no symlinks. `sessions` holds 5 `*.json` files and 5 key files; every key name matches `<pid>.<64 lowercase hex>.key`. Each json has integer `pid`, `sessionId`, and a millisecond `startedAt` (13 digits). The filename stem equals `pid` in all 5. All 5 pids were alive at the read. `jobs` holds 2 real directories, both 8 characters long, and each `state.json` `sessionId` starts with that directory name. No job entry is a symlink. `projects` has 16 keys, none of them symlinks; across those keys the entries include 25 `<uuid>.jsonl` files and 7 `<uuid>` directories, plus shared names such as the memory directory the purge leaves alone.
`/tmp` is a symlink to `/private/tmp`. `/tmp/claude-<uid>` and `/private/tmp/claude-<uid>` both exist and share one real path. Under that root, project keys are real directories. Their children are uuid directories (16) plus 3 other directories whose names are 57 or 58 alphanumeric characters, contain no uuid, and are siblings of the uuid scratchpads. `$TMPDIR/claude-<uid>` does not exist. `CLAUDE_CODE_TMPDIR` was unset in this review process. That is the layout `_tmp_roots` (`claude.py:25-48`) and `_owned_paths` (`claude.py:256-285`) assume: an existing per-uid root, then `<key>/<uuid>`.
## lstart on this host
`ps -p <pid> -o lstart=` with `LC_ALL=C` prints five fields. `time.strptime` of the collapsed line with `%a %b %d %H:%M:%S %Y` accepts both `Oct 5` and the BSD space-padded `Oct 5`. For the five live session files, `lstart - startedAt/1000` is between −2.43 s and −0.694 s, so `lstart <= startedAt/1000 + 2` (`claude.py:77`) holds for every one of them. Zero of those live files would be treated as a reused pid.
A temp-home probe of `_session_file_live`: the current pid with `startedAt` set to now is live; the same pid with `startedAt` 1.5 s after its own `lstart` is live; missing `startedAt`, `startedAt` 0, a non-numeric `startedAt`, `pid` 0, a negative pid, and boolean `True` are live. A dead pid is stale. The same live pid with `startedAt` an hour earlier, or 3 s earlier than its `lstart`, is stale. The process locale reported `en_US` / `UTF-8`, and the English month from `LC_ALL=C` parsed. A parse failure returns live (`claude.py:71-72`), which refuses the purge.
## Deletion stays on this session
On a temporary config root the purge removed the workspace `<uuid>.jsonl`, the sibling `<uuid>/` directory, `file-history/<uuid>/`, `session-env/<uuid>/`, `jobs/<uuid[:8]>/` whose `state.json` named the session, the dead `sessions/<pid>.json`, its `<pid>.<64 hex>.key`, and `<CLAUDE_CODE_TMPDIR>/claude-<uid>/<key>/<uuid>/`. The printed key label contains `<pid>.*.key` and the hex is absent from the purged list.
These stayed: another session's `sessions/<pid>.json` and key for a live pid, that session's scratchpad, `history.jsonl` whose body names the purged uuid, `projects/<key>/memory/MEMORY.md`, and a job directory whose 8-character name matches the uuid but whose `state.json` names a different session (`claude.py:244-254`). `_plausible_id` (`base.py:89-92`) rejects `""`, `.`, `..`, `*`, `../x`, `a/b`, and `....` (`".."` is a substring). Purging those ids left the tree unchanged.
Symlinks. A `file-history/<uuid>` symlink to an outside directory was unlinked; the outside `secret.txt` remained. A `projects/<key>/<uuid>` symlink to an outside directory was unlinked; the outside file remained. A directory symlink and a file symlink inside a real owned `<uuid>/` directory were removed with the directory; both targets remained (`_remove`, `claude.py:80-87`, uses `os.path.islink` and then `os.remove`; `shutil.rmtree` sees the real directory). A `jobs/<8>` symlink whose target `state.json` names the session was unlinked; the target directory and its `other.txt` remained (`_remove_job`, `claude.py:107-109`). A `sessions/<pid>.json` symlink to an outside json was unlinked after the read saw this session id; the outside file remained.
A project key under the temp root that is itself a symlink is followed: `<symlink>/<uuid>` was removed on the far side. That far-side directory is still the one named this session's uuid, which is the scratchpad path when a key has been relocated. The real per-uid temp root on this Mac has no key symlinks. The cross-session paths above stayed in the same probe.
A live session file is omitted from `_owned_paths` (`claude.py:274-278`). Calling `purge_artifacts` directly while that pid was alive left `sessions/<pid>.json` in place and removed `file-history/<uuid>/`. After the process exited, the same call removed the session file. The purge CLI does not take that direct path: `in-use` was true, the process exited 4, and the file-history marker was still there (`__main__.py:146-148`). `in-use` with no holder exits 0. `in-use` without a uuid exits 1.
## Stop
`stop_session.sh:285-300` runs `python -m lib_py.agents in-use <agent> <uuid> <cwd> --wait 5` before the registry write. Any status other than 0 exits 4 and leaves the row. The registry python then prints `purged:` lines and either `WARN residual:` or `residual: none (agent: <agent>)` (`stop_session.sh:355-368`). `lib.sh` exports `PYTHONPATH` to the skill dir at source time, so the new `in-use` module resolves the same way `exit-key` already does. `DELEGATE_JOB_ID`, `DELEGATE_JOB_TOKEN`, and `DELEGATE_JOB_ATTEMPT` are set before that call (`stop_session.sh:146-161`), including to empty strings, so `set -u` does not trip. The passthrough `raise SystemExit(1)` is now `stop_session.sh:327`, and `tests/data/skill_code_blocks.json` records that line. The stop SKILL.md diagram and the purge paragraph match the fenced blocks in that json (the skill-block test passed).
## Tests
Pytest used the control-plane venv, `PYTHONDONTWRITEBYTECODE=1`, `-p no:cacheprovider`, and `--basetemp` `/tmp/mamf0-f0df1be0-work/pybase`. Inherited `HERDR_*`, `MAM_*`, `MQTT_*`, `SKS_*`, `WORKSPACE_ROOT`, `DELEGATE_JOB_LOGS_DIR`, `HOME_DIR`, `CLAUDE_PROJECT_DIR`, `CLAUDE_CONFIG_DIR`, `CLAUDE_CODE_TMPDIR`, `CODEX_HOME`, and `PYTHONPATH` were dropped. The suite's own fixtures then point `HOME` at the test tmp.
On the snapshot, 80 tests passed and 1 skipped in 28.31 s: all of `tests/test_issue31_purge_residual.py` (32 tests), `tests/test_issue42_skill_blocks.py`, and `tests/test_issue55_agent_homes.py`. The skip is `test_issue42_skill_blocks.py:266`, because `.claude/skills` is absent in this worktree. The issue-31 file covers the owned-path purge, the untouched neighbor files, the same-prefix job, malformed ids, the live-process refusal, the reused pid, a key that cannot be unlinked, a job directory that cannot be emptied, `CLAUDE_CONFIG_DIR`, the temp-root rules, the stop exit 4, the stray-key `WARN residual:`, and codex, grok, and opencode residuals.
The same issue-31 file copied onto an archive of base `26e2c43` failed 24 tests and passed 8 in 17.09 s. The failures are behavioral. The old purge left `file-history/<uuid>` and `session-env/<uuid>` behind. The old purge CLI, asked about a live pid whose argv does not contain the uuid, removed the jsonl and returned `in_use` false. The old stop did not exit 4 for that live process and did not print `WARN residual:` for a transcript under another project key. Two failures are the new helpers `_session_file_live` and `_tmp_roots` being absent; the assertions above fail on the old behavior itself.
After the report, snapshot porcelain is still empty, the snapshot still has no `.mam`, and control-plane porcelain is still empty at `26e2c43`. The herdr config mtime is still 1784456394.
[VERDICT: PASS]