Files
skills/skills/kvb-transcend/scripts/transcend.py
T
Kevin Veen-BirkenbachandClaude Opus 5.5 2fbbd7386c feat(transcend): carry session scripts, tests and skills into the repos
Adds kvb-transcend (collector), kvb-skills-transcend (into the skills repo) and kvb-code-transcend (into the current repo). transcend.py scans the own scratchpad, peer scratchpads and the agent homes for scripts, tests and skills the skills repo does not ship, skips secret-named and oversized files, and remembers carried/dropped decisions by content digest in the gitignored .state/transcend.json, so a rescan offers only new or changed candidates. Wired into kvb-skills-tune (own scripts), kvb-code-tune step 6 (measurements) and kvb-skills-dream step 7. Seven descriptions are tightened to stay inside the 1300-word listing budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:57:14 +01:00

188 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Find scripts, tests and skills worth carrying over, and remember every decision.
Exit codes:
0 done, 2 usage error.
"""
from __future__ import annotations
import argparse
import datetime
import hashlib
import json
import os
import re
import sys
from pathlib import Path
SCRIPT_SUFFIXES = {".py", ".sh", ".bash", ".js", ".mjs", ".ts", ".rb", ".pl"}
TEST_NAME = re.compile(r"(^test_.+|.+_test|.+\.(spec|test))\.[a-z]+$")
SECRET_NAME = re.compile(
r"(secret|token|credential|passw|auth|\.env$|id_[a-z0-9]+$|\.pem$|\.key$)", re.IGNORECASE
)
MAX_BYTES = 1_000_000
HOME_SOURCES = {
".claude/skills": "skill",
".agents/skills": "skill",
".codex/skills": "skill",
".claude/commands": "command",
".claude/agents": "agent",
".claude/hooks": "script",
".claude/scripts": "script",
".codex/rules": "rule",
}
SETTLED = ("carried", "dropped")
VERDICTS = (*SETTLED, "later")
def fail(message: str) -> None:
print(f"transcend: {message}", file=sys.stderr)
sys.exit(2)
def digest(path: Path) -> str:
sha = hashlib.sha256()
files = sorted(p for p in path.rglob("*") if p.is_file()) if path.is_dir() else [path]
for file in files:
sha.update(str(file.relative_to(path) if path.is_dir() else file.name).encode())
sha.update(file.read_bytes())
return sha.hexdigest()
def file_kind(path: Path) -> str | None:
if SECRET_NAME.search(path.name) or path.stat().st_size > MAX_BYTES:
return None
if TEST_NAME.match(path.name):
return "test"
if path.suffix in SCRIPT_SUFFIXES or os.access(path, os.X_OK):
return "script"
return None
def session_names(home: Path) -> dict[str, str]:
names = {}
for path in (home / ".claude" / "sessions").glob("*.json"):
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
continue
if data.get("sessionId"):
names[data["sessionId"]] = data.get("name") or data["sessionId"]
return names
def scratch_candidates(root: Path, source: str, owner: str) -> list[dict]:
found = []
for path in sorted(p for p in root.rglob("*") if p.is_file()):
kind = file_kind(path)
if kind:
found.append({"source": source, "owner": owner, "kind": kind, "path": str(path)})
return found
def shipped(skills_repo: Path) -> set[str]:
names = {p.name for p in (skills_repo / "skills").iterdir() if p.is_dir()}
lock = json.loads((skills_repo / "skills-lock.json").read_text(encoding="utf-8"))
return names | set(lock.get("skills", {}))
def home_candidates(home: Path, skills_repo: Path) -> list[dict]:
known = shipped(skills_repo)
found = []
for sub, kind in HOME_SOURCES.items():
base = home / sub
if not base.is_dir():
continue
for entry in sorted(base.iterdir()):
if entry.name.startswith(".") or SECRET_NAME.search(entry.name):
continue
if kind == "skill" and (entry.name in known or not (entry / "SKILL.md").is_file()):
continue
if kind != "skill" and not entry.is_file():
continue
found.append({"source": "home", "owner": "operator", "kind": kind, "path": str(entry)})
return found
def load_state(path: Path) -> dict[str, dict]:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
return {}
except ValueError:
fail(f"{path} is not valid JSON")
def cmd_scan(args: argparse.Namespace) -> int:
session = Path(args.session_dir).resolve()
home = Path(args.home)
names = session_names(home)
candidates = scratch_candidates(session, "own", "self")
for scratch in sorted(Path(args.peers_root).glob("*/*/scratchpad")):
if scratch.resolve() == session:
continue
sid = scratch.parent.name
candidates += scratch_candidates(scratch, "peer", names.get(sid, sid))
candidates += home_candidates(home, Path(args.skills_repo))
state = load_state(Path(args.state))
offered = []
for item in candidates:
seen = state.get(item["path"])
item["digest"] = digest(Path(item["path"]))
if seen and seen["verdict"] in SETTLED and seen["digest"] == item["digest"]:
continue
item["previous"] = seen["verdict"] if seen else None
offered.append(item)
if args.json:
print(json.dumps(offered, indent=1))
else:
for item in offered:
note = f"\t(was {item['previous']})" if item["previous"] else ""
print(f"{item['source']}\t{item['owner']}\t{item['kind']}\t{item['path']}{note}")
return 0
def cmd_mark(args: argparse.Namespace) -> int:
target = Path(args.path)
if not target.exists():
fail(f"{target} does not exist")
state_path = Path(args.state)
state = load_state(state_path)
state[str(target)] = {
"verdict": args.verdict,
"digest": digest(target),
"at": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"),
}
state_path.parent.mkdir(parents=True, exist_ok=True)
tmp = state_path.with_suffix(".tmp")
tmp.write_text(json.dumps(state, indent=1, sort_keys=True), encoding="utf-8")
tmp.replace(state_path)
print(f"{args.verdict} {target}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
sub = parser.add_subparsers(dest="cmd", required=True)
p = sub.add_parser("scan")
p.add_argument("--state", required=True)
p.add_argument("--skills-repo", required=True)
p.add_argument("--session-dir", required=True)
p.add_argument("--peers-root", required=True)
p.add_argument("--home", required=True)
p.add_argument("--json", action="store_true")
p.set_defaults(func=cmd_scan)
p = sub.add_parser("mark")
p.add_argument("--state", required=True)
p.add_argument("path")
p.add_argument("verdict", choices=VERDICTS)
p.set_defaults(func=cmd_mark)
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
sys.exit(main())