mirror of
https://github.com/alirezarezvani/claude-skills.git
synced 2026-10-06 02:50:08 +00:00
Ships the three improvements judged most impactful in v1.1 design review:
1. SessionEnd hook (hooks/session_end.py)
Pairs with SessionStart. When a session ends with no handoff in the
last 30 minutes, prints a one-line reminder. Cannot prompt
interactively or block session end — surfaces text via stdout.
Disable per-session with HANDOFF_SESSIONEND=0. hooks.json updated to
wire both SessionStart and SessionEnd.
2. handoff_self_check.py — fidelity script (~300 LOC, stdlib-only)
Operationalizes handoff_prompt.md. Six checks:
- All 5 sections present
- Goal is non-empty and non-placeholder
- State-of-play bullets reference at least one artifact (commit hash,
PR/issue number, file path, URL)
- Open decisions are present (or explicit "- None.") when git is dirty
or has recent commits
- Skills to use: 3-5 entries, hard cap enforced
- Artifacts contain paths/URLs only, no inline content
Severity: high/medium/low. Strict mode exits 1 only on HIGH findings.
--sample fixture has 3 planted issues (2 high + 1 medium) and exits 1.
Canonical example_handoff.md passes clean (exit 0).
/cs:handoff command updated to run self-check between scaffold-fill
and redaction linter.
3. --refresh flag on handoff_template_generator.py
Reuses the most recent handoff in the configured save location
instead of creating a new file. Falls through to create-if-missing
when no existing handoff is found. Keeps the save location
uncluttered when work continues past the original handoff time;
ensures the SessionStart hook always loads the up-to-date version.
Version bump: 2.7.4 -> 2.7.5. Marketplace description and keywords
updated. README v1.1 section added. SKILL.md gains "Refreshing an
Existing Handoff" and "SessionEnd Reminder" subsections.
Verified:
- All 9 Python files compile clean
- self-check --sample correctly fails (3 findings, exit 1)
- self-check passes clean against assets/example_handoff.md (exit 0)
- --refresh finds the latest /tmp/handoff-*.md and prints its path
- SessionEnd hook prints the reminder when no recent handoff exists
- check_plugin_json.py + marketplace.json + hooks.json all parse
- Plugin audit re-run: structure 84.2 -> 86.0, quality 62.2 -> 63.0,
security PASS (0 critical, 0 high)
- Codex + Gemini sync re-ran clean
https://claude.ai/code/session_01KLhHBAfEDXdQMeRe6G8sRa
115 lines
3.1 KiB
Python
115 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""SessionEnd hook for the handoff skill.
|
|
|
|
When a session ends, checks whether a recent handoff exists in the
|
|
configured save location. If none does (or the most recent is older than
|
|
the staleness threshold), prints a one-line reminder so the user
|
|
remembers to write one before context is lost.
|
|
|
|
Cannot block session end and cannot prompt interactively — the reminder
|
|
is printed to stdout, which Claude Code surfaces in the session log.
|
|
|
|
Disable per-session with HANDOFF_SESSIONEND=0.
|
|
|
|
Stdlib-only.
|
|
"""
|
|
|
|
import datetime as dt
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
SKILL_SCRIPTS = (
|
|
Path(__file__).resolve().parent.parent / "skills" / "handoff" / "scripts"
|
|
)
|
|
sys.path.insert(0, str(SKILL_SCRIPTS))
|
|
|
|
try:
|
|
import config_loader # type: ignore
|
|
except ImportError:
|
|
sys.exit(0)
|
|
|
|
|
|
HANDOFF_TOKENS = ("handoff-", "handoff_", "handoff.md")
|
|
STALE_AFTER_MINUTES = 30 # If no handoff in this window, remind.
|
|
|
|
|
|
def _disabled() -> bool:
|
|
return os.environ.get("HANDOFF_SESSIONEND", "1") == "0"
|
|
|
|
|
|
def _candidate_dirs(config: dict) -> list[Path]:
|
|
save = config.get("save_location", {})
|
|
mode = save.get("mode", "temp")
|
|
raw_path = save.get("path")
|
|
dirs: list[Path] = []
|
|
if mode == "temp":
|
|
dirs.append(Path(tempfile.gettempdir()))
|
|
elif raw_path:
|
|
dirs.append(Path(raw_path))
|
|
proj = Path.cwd() / ".handoff"
|
|
if proj.exists() and proj not in dirs:
|
|
dirs.append(proj)
|
|
return [d for d in dirs if d.exists()]
|
|
|
|
|
|
def _looks_like_handoff(p: Path) -> bool:
|
|
name = p.name.lower()
|
|
return name.endswith(".md") and any(t in name for t in HANDOFF_TOKENS)
|
|
|
|
|
|
def _latest_mtime(config: dict) -> dt.datetime | None:
|
|
latest: float | None = None
|
|
for d in _candidate_dirs(config):
|
|
try:
|
|
for entry in d.iterdir():
|
|
if not entry.is_file() or not _looks_like_handoff(entry):
|
|
continue
|
|
try:
|
|
mtime = entry.stat().st_mtime
|
|
except OSError:
|
|
continue
|
|
if latest is None or mtime > latest:
|
|
latest = mtime
|
|
except OSError:
|
|
continue
|
|
if latest is None:
|
|
return None
|
|
return dt.datetime.utcfromtimestamp(latest)
|
|
|
|
|
|
def _remind() -> None:
|
|
print("=" * 60)
|
|
print("Session ending — no recent handoff detected.")
|
|
print()
|
|
print("Want to capture state for next time? In a future session, run:")
|
|
print(" /cs:handoff \"<what the next session should do>\"")
|
|
print()
|
|
print("Disable this reminder: HANDOFF_SESSIONEND=0")
|
|
print("=" * 60)
|
|
|
|
|
|
def main() -> int:
|
|
if _disabled():
|
|
return 0
|
|
try:
|
|
config = config_loader.load_config()
|
|
except Exception:
|
|
return 0
|
|
try:
|
|
latest = _latest_mtime(config)
|
|
except Exception:
|
|
return 0
|
|
now = dt.datetime.utcnow()
|
|
stale_cutoff = now - dt.timedelta(minutes=STALE_AFTER_MINUTES)
|
|
if latest is None or latest < stale_cutoff:
|
|
try:
|
|
_remind()
|
|
except Exception:
|
|
return 0
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|