diff --git a/.agents/skills/lib_py/agents/adapters/claude.py b/.agents/skills/lib_py/agents/adapters/claude.py index 480ed52..a093a46 100644 --- a/.agents/skills/lib_py/agents/adapters/claude.py +++ b/.agents/skills/lib_py/agents/adapters/claude.py @@ -344,8 +344,10 @@ class ClaudeAgentAdapter(BaseAgentAdapter): return [p for root, p in self._candidates(uuid, ctx) if _inside(root, p)] def has_artifacts(self, uuid: str, ctx: DiscoveryContext) -> bool: - # Leftovers count without the transcript (a pre-#31 purge left them). - return bool(self._owned_paths(uuid, ctx)) + # Leftovers count without the transcript (a pre-#31 purge left them), + # and so does one behind a link: the purge keeps it, but picking + # claude is what gets it reported as residual. + return bool(self._candidates(uuid, ctx)) def conversation_in_use(self, uuid: str, ctx: DiscoveryContext) -> bool: # No claude process holds a name that is not a uuid; searching diff --git a/tests/test_issue31_purge_residual.py b/tests/test_issue31_purge_residual.py index 5d7bca5..b48590f 100644 --- a/tests/test_issue31_purge_residual.py +++ b/tests/test_issue31_purge_residual.py @@ -355,6 +355,26 @@ def test_claude_purge_never_follows_a_linked_dir_below_a_root(home, tmp_path, co str(cfg / "projects" / key / conv), str(cfg / "file-history" / conv)]) +def test_purge_cli_reports_a_linked_orphan_it_will_not_remove(home, tmp_path, conv): + """A candidate behind a link is all that is left: the CLI still picks + claude, so its residual names it, and removes nothing.""" + ws = tmp_path / "ws" + ws.mkdir() + keep = tmp_path / "outside" / conv / "keep.txt" + keep.parent.mkdir(parents=True) + keep.write_text("keep") + troot = tmp_path / "ctmp" / f"claude-{UID}" + troot.mkdir(parents=True) + (troot / "linked-workspace").symlink_to(tmp_path / "outside") + + res = _agents_cli(["purge", "claude", conv, str(ws)], ws) + + assert res.returncode == 0, res.stdout + res.stderr + assert json.loads(res.stdout) == {"agent": "claude", "purged": [], + "residual": [str(troot / "linked-workspace" / conv)], "in_use": False} + assert keep.exists() + + def test_claude_purge_removes_only_the_link_at_the_leaf(home, tmp_path, conv): ws = tmp_path / "ws" ws.mkdir()