pkgmgr gc --report lists each linked worktree and orphaned .claude/worktrees/agent-* dir with its size and last activity, the changes a cleanup would keep, the biggest children of every --root, ELF core dumps, docker system df with --docker, and free versus attributed space per filesystem; --json prints the same as JSON. It deletes and writes nothing and refuses --apply. git run() gains input_text for cat-file --batch-check. 🎯 Fix: 90 % 💥 Break: 2 % 💸 Cost: new ⏱️ Time: new 🛡️ Security: 100 % 🏆 Score: 88 🚦 Gate: cost new, time new: cleared by the operator's commit word for pkgmgr gc --report (2026-11-07) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
2.8 KiB
Python
97 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
from pkgmgr.actions.branch.gc import _worktrees
|
|
|
|
from .walk import TreeStats, walk
|
|
|
|
AGENT_DIR = os.path.join(".claude", "worktrees")
|
|
AGENT_PREFIX = "agent-"
|
|
METADATA_FILES = ("index", "HEAD")
|
|
|
|
|
|
def _metadata_mtime(path: str) -> float:
|
|
try:
|
|
with open(os.path.join(path, ".git"), encoding="utf-8") as fh:
|
|
gitdir = fh.read().strip().removeprefix("gitdir: ")
|
|
except OSError:
|
|
return 0.0
|
|
meta = os.path.normpath(os.path.join(path, gitdir))
|
|
stamps = [0.0]
|
|
for name in METADATA_FILES:
|
|
try:
|
|
stamps.append(os.stat(os.path.join(meta, name)).st_mtime)
|
|
except OSError:
|
|
continue
|
|
return max(stamps)
|
|
|
|
|
|
def _record(
|
|
repo: str,
|
|
path: str,
|
|
branch: str | None,
|
|
head: str | None,
|
|
orphaned: bool,
|
|
cores: list[str],
|
|
) -> dict:
|
|
present = os.path.isdir(path)
|
|
stats = walk(path) if present else TreeStats()
|
|
cores.extend(stats.cores)
|
|
activity = max(stats.newest_mtime, _metadata_mtime(path)) if present else 0.0
|
|
return {
|
|
"repo": repo,
|
|
"path": path,
|
|
"branch": branch,
|
|
"head": head,
|
|
"orphaned": orphaned,
|
|
"missing": not present,
|
|
"size_bytes": stats.size_bytes if present else None,
|
|
"last_activity": activity or None,
|
|
"foreign_entries": stats.foreign_entries,
|
|
"unreadable": stats.unreadable,
|
|
}
|
|
|
|
|
|
def _agent_dirs(repo: str) -> list[str]:
|
|
base = os.path.join(repo, AGENT_DIR)
|
|
try:
|
|
names = sorted(os.listdir(base))
|
|
except OSError:
|
|
return []
|
|
return [
|
|
os.path.join(base, name)
|
|
for name in names
|
|
if name.startswith(AGENT_PREFIX)
|
|
and os.path.isdir(os.path.join(base, name))
|
|
and not os.path.islink(os.path.join(base, name))
|
|
]
|
|
|
|
|
|
def list_worktrees(repo: str, cores: list[str]) -> list[dict]:
|
|
"""
|
|
Inventory the linked worktrees of one repository, orphaned agent dirs included.
|
|
|
|
Args:
|
|
repo: main worktree root of the repository.
|
|
cores: receives the core dump candidates found while measuring.
|
|
|
|
Returns:
|
|
One record per linked worktree git lists and per `.claude/worktrees/agent-*`
|
|
directory it does not list (orphaned), sorted by path.
|
|
|
|
Raises:
|
|
GitBaseError: when `git worktree list` fails.
|
|
"""
|
|
trees = [tree for tree in _worktrees(repo) if not tree.main]
|
|
listed = {os.path.realpath(tree.path) for tree in trees}
|
|
records = [
|
|
_record(repo, tree.path, tree.branch, tree.head, False, cores) for tree in trees
|
|
]
|
|
records += [
|
|
_record(repo, path, None, None, True, cores)
|
|
for path in _agent_dirs(repo)
|
|
if os.path.realpath(path) not in listed
|
|
]
|
|
return sorted(records, key=lambda record: record["path"])
|