session-handoff
Creates comprehensive handoff documents for seamless AI agent session transfers. Triggered when: (1) user requests handoff/memory/context save, (2) context window approaches capacity, (3) major task milestone completed, (4) work session ending, (5) user says 'save state', 'create handoff', 'I need to pause', 'context is getting full', (6) resuming work with 'load handoff', 'resume from', 'continue where we left off'. Proactively suggests handoffs after substantial work (multiple file edits, complex debugging, architecture decisions). Solves long-running agent context exhaustion by enabling fresh agents to continue with zero ambiguity.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install softaworks-agent-toolkit-session-handoff
Repository
Skill path: skills/session-handoff
Creates comprehensive handoff documents for seamless AI agent session transfers. Triggered when: (1) user requests handoff/memory/context save, (2) context window approaches capacity, (3) major task milestone completed, (4) work session ending, (5) user says 'save state', 'create handoff', 'I need to pause', 'context is getting full', (6) resuming work with 'load handoff', 'resume from', 'continue where we left off'. Proactively suggests handoffs after substantial work (multiple file edits, complex debugging, architecture decisions). Solves long-running agent context exhaustion by enabling fresh agents to continue with zero ambiguity.
Open repositoryBest for
Primary workflow: Analyze Data & AI.
Technical facets: Full Stack, Data / AI.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: softaworks.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install session-handoff into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/softaworks/agent-toolkit before adding session-handoff to shared team environments
- Use session-handoff for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: session-handoff
description: "Creates comprehensive handoff documents for seamless AI agent session transfers. Triggered when: (1) user requests handoff/memory/context save, (2) context window approaches capacity, (3) major task milestone completed, (4) work session ending, (5) user says 'save state', 'create handoff', 'I need to pause', 'context is getting full', (6) resuming work with 'load handoff', 'resume from', 'continue where we left off'. Proactively suggests handoffs after substantial work (multiple file edits, complex debugging, architecture decisions). Solves long-running agent context exhaustion by enabling fresh agents to continue with zero ambiguity."
---
# Handoff
Creates comprehensive handoff documents that enable fresh AI agents to seamlessly continue work with zero ambiguity. Solves the long-running agent context exhaustion problem.
## Mode Selection
Determine which mode applies:
**Creating a handoff?** User wants to save current state, pause work, or context is getting full.
- Follow: CREATE Workflow below
**Resuming from a handoff?** User wants to continue previous work, load context, or mentions an existing handoff.
- Follow: RESUME Workflow below
**Proactive suggestion?** After substantial work (5+ file edits, complex debugging, major decisions), suggest:
> "We've made significant progress. Consider creating a handoff document to preserve this context for future sessions. Say 'create handoff' when ready."
## CREATE Workflow
### Step 1: Generate Scaffold
Run the smart scaffold script to create a pre-filled handoff document:
```bash
python scripts/create_handoff.py [task-slug]
```
Example: `python scripts/create_handoff.py implementing-user-auth`
**For continuation handoffs** (linking to previous work):
```bash
python scripts/create_handoff.py "auth-part-2" --continues-from 2024-01-15-auth.md
```
The script will:
- Create `.claude/handoffs/` directory if needed
- Generate timestamped filename
- Pre-fill: timestamp, project path, git branch, recent commits, modified files
- Add handoff chain links if continuing from previous
- Output file path for editing
### Step 2: Complete the Handoff Document
Open the generated file and fill in all `[TODO: ...]` sections. Prioritize these sections:
1. **Current State Summary** - What's happening right now
2. **Important Context** - Critical info the next agent MUST know
3. **Immediate Next Steps** - Clear, actionable first steps
4. **Decisions Made** - Choices with rationale (not just outcomes)
Use the template structure in [references/handoff-template.md](references/handoff-template.md) for guidance.
### Step 3: Validate the Handoff
Run the validation script to check completeness and security:
```bash
python scripts/validate_handoff.py <handoff-file>
```
The validator checks:
- [ ] No `[TODO: ...]` placeholders remaining
- [ ] Required sections present and populated
- [ ] No potential secrets detected (API keys, passwords, tokens)
- [ ] Referenced files exist
- [ ] Quality score (0-100)
**Do not finalize a handoff with secrets detected or score below 70.**
### Step 4: Confirm Handoff
Report to user:
- Handoff file location
- Validation score and any warnings
- Summary of captured context
- First action item for next session
## RESUME Workflow
### Step 1: Find Available Handoffs
List handoffs in the current project:
```bash
python scripts/list_handoffs.py
```
This shows all handoffs with dates, titles, and completion status.
### Step 2: Check Staleness
Before loading, check how current the handoff is:
```bash
python scripts/check_staleness.py <handoff-file>
```
Staleness levels:
- **FRESH**: Safe to resume - minimal changes since handoff
- **SLIGHTLY_STALE**: Review changes, then resume
- **STALE**: Verify context carefully before resuming
- **VERY_STALE**: Consider creating a fresh handoff
The script checks:
- Time since handoff was created
- Git commits since handoff
- Files changed since handoff
- Branch divergence
- Missing referenced files
### Step 3: Load the Handoff
Read the relevant handoff document completely before taking any action.
If handoff is part of a chain (has "Continues from" link), also read the linked previous handoff for full context.
### Step 4: Verify Context
Follow the checklist in [references/resume-checklist.md](references/resume-checklist.md):
1. Verify project directory and git branch match
2. Check if blockers have been resolved
3. Validate assumptions still hold
4. Review modified files for conflicts
5. Check environment state
### Step 5: Begin Work
Start with "Immediate Next Steps" item #1 from the handoff document.
Reference these sections as you work:
- "Critical Files" for important locations
- "Key Patterns Discovered" for conventions to follow
- "Potential Gotchas" to avoid known issues
### Step 6: Update or Chain Handoffs
As you work:
- Mark completed items in "Pending Work"
- Add new discoveries to relevant sections
- For long sessions: create a new handoff with `--continues-from` to chain them
## Handoff Chaining
For long-running projects, chain handoffs together to maintain context lineage:
```
handoff-1.md (initial work)
↓
handoff-2.md --continues-from handoff-1.md
↓
handoff-3.md --continues-from handoff-2.md
```
Each handoff in the chain:
- Links to its predecessor
- Can mark older handoffs as superseded
- Provides context breadcrumbs for new agents
When resuming from a chain, read the most recent handoff first, then reference predecessors as needed.
## Storage Location
Handoffs are stored in: `.claude/handoffs/`
Naming convention: `YYYY-MM-DD-HHMMSS-[slug].md`
Example: `2024-01-15-143022-implementing-auth.md`
## Resources
### scripts/
| Script | Purpose |
|--------|---------|
| `create_handoff.py [slug] [--continues-from <file>]` | Generate new handoff with smart scaffolding |
| `list_handoffs.py [path]` | List available handoffs in a project |
| `validate_handoff.py <file>` | Check completeness, quality, and security |
| `check_staleness.py <file>` | Assess if handoff context is still current |
### references/
- [handoff-template.md](references/handoff-template.md) - Complete template structure with guidance
- [resume-checklist.md](references/resume-checklist.md) - Verification checklist for resuming agents
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/handoff-template.md
```markdown
# Handoff Template
Use this template structure when creating handoff documents. The smart scaffold script will pre-fill metadata sections; complete the remaining sections based on session context.
## Table of Contents
- [Session Metadata](#session-metadata)
- [Current State Summary](#current-state-summary)
- [Codebase Understanding](#codebase-understanding)
- [Architecture Overview](#architecture-overview)
- [Critical Files](#critical-files)
- [Key Patterns Discovered](#key-patterns-discovered)
- [Work Completed](#work-completed)
- [Tasks Finished](#tasks-finished)
- [Files Modified](#files-modified)
- [Decisions Made](#decisions-made)
- [Pending Work](#pending-work)
- [Immediate Next Steps](#immediate-next-steps)
- [Blockers/Open Questions](#blockersopen-questions)
- [Deferred Items](#deferred-items)
- [Context for Resuming Agent](#context-for-resuming-agent)
- [Important Context](#important-context)
- [Assumptions Made](#assumptions-made)
- [Potential Gotchas](#potential-gotchas)
- [Environment State](#environment-state)
- [Related Resources](#related-resources)
- [Template Usage Notes](#template-usage-notes)
---
# Handoff: [TASK_TITLE]
## Session Metadata
- Created: [TIMESTAMP]
- Project: [PROJECT_PATH]
- Branch: [GIT_BRANCH]
- Session duration: [APPROX_DURATION]
## Current State Summary
[One paragraph: What was being worked on, current status, and where things left off]
## Codebase Understanding
### Architecture Overview
[Key architectural insights discovered during this session - how the system is structured, main components, data flow]
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| path/to/file | What this file does | Why it matters for this task |
### Key Patterns Discovered
[Important patterns, conventions, or idioms found in this codebase that the next agent should follow]
## Work Completed
### Tasks Finished
- [x] Task 1 - brief description of what was done
- [x] Task 2 - brief description
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
| path/to/file | Description of changes | Why this change was made |
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| Chose X over Y | X, Y, Z | Why X was chosen |
## Pending Work
### Immediate Next Steps
1. [Most critical next action - what to do first]
2. [Second priority]
3. [Third priority]
### Blockers/Open Questions
- [ ] Blocker: [description] - Needs: [what's required to unblock]
- [ ] Question: [unclear aspect] - Suggested: [potential resolution]
### Deferred Items
- Item 1 (deferred because: [reason, e.g., out of scope, needs user input])
## Context for Resuming Agent
### Important Context
[Critical information the next agent MUST know to continue effectively - this is the most important section for handoff]
### Assumptions Made
- Assumption 1: [what was assumed to be true]
- Assumption 2: [another assumption]
### Potential Gotchas
- [Things that might trip up a new agent - edge cases, quirks, non-obvious behavior]
## Environment State
### Tools/Services Used
- [Tool/Service]: [relevant configuration or state]
### Active Processes
- [Any background processes, dev servers, watchers that may be running]
### Environment Variables
- [Key env vars that matter for this work - DO NOT include secrets/values, just names]
## Related Resources
- [Link to relevant documentation]
- [Related file paths]
- [External resources consulted]
---
## Template Usage Notes
When filling this template:
1. Be specific and concrete - vague descriptions don't help the next agent
2. Include file paths with line numbers where relevant (e.g., `src/auth.ts:142`)
3. Prioritize the "Important Context" and "Immediate Next Steps" sections
4. Don't include sensitive data (API keys, passwords, tokens)
5. Focus on WHAT and WHY, not just WHAT - rationale is crucial for handoffs
```
### references/resume-checklist.md
```markdown
# Resume Checklist
Follow this checklist when resuming work from a handoff document to ensure zero-ambiguity continuation.
## Pre-Resume Verification
- [ ] Read the entire handoff document before taking any action
- [ ] Verify you are in the correct project directory
- [ ] Confirm the git branch matches (or understand why it might differ)
- [ ] Check the handoff timestamp - how stale is this context?
## Context Validation
- [ ] Review "Important Context" section thoroughly
- [ ] Understand all assumptions listed - are they still valid?
- [ ] Check if any blockers have been resolved since handoff
- [ ] Review "Potential Gotchas" to avoid known pitfalls
## State Verification
- [ ] Run `git status` to see current file state
- [ ] Compare modified files list in handoff vs current state
- [ ] Check if any environment variables need to be set
- [ ] Verify any required services/processes are running
## Resume Execution
- [ ] Start with "Immediate Next Steps" item #1
- [ ] Reference "Files Modified" table for context on recent changes
- [ ] Apply patterns documented in "Key Patterns Discovered"
- [ ] Follow architectural insights from "Architecture Overview"
## During Work
- [ ] Update handoff document if major new context is discovered
- [ ] Mark completed items in "Pending Work" as you finish them
- [ ] Add new blockers/questions as they arise
- [ ] Consider creating a new handoff if session becomes long
## Red Flags - Stop and Verify
If you encounter any of these, pause and verify context before proceeding:
1. **Files mentioned in handoff don't exist** - codebase may have changed significantly
2. **Branch has diverged substantially** - check git log for recent commits
3. **Assumptions are clearly invalid** - reassess the approach
4. **Blockers marked as unresolved are now blocking you** - escalate to user
5. **Architecture has changed** - re-explore before continuing
## Quick Start Commands
After reading the handoff, these commands help verify state:
```bash
# Check current branch and status
git branch --show-current
git status
# See recent commits (compare with handoff)
git log --oneline -10
# Check for any running processes mentioned
ps aux | grep [process-name]
# Verify environment
env | grep [relevant-var]
```
## Handoff Quality Assessment
Rate the handoff quality to identify if more exploration is needed:
| Aspect | Good | Needs Exploration |
|--------|------|-------------------|
| Next steps | Clear, actionable | Vague or missing |
| File references | Specific paths/lines | General descriptions |
| Decisions | Rationale included | Just outcomes |
| Context | Complete picture | Gaps or assumptions |
If multiple aspects "Need Exploration", spend time re-exploring the codebase before continuing implementation.
```
### scripts/create_handoff.py
```python
#!/usr/bin/env python3
"""
Smart scaffold generator for handoff documents.
Creates a new handoff document with auto-detected metadata:
- Current timestamp
- Project path
- Git branch (if available)
- Recent git log
- Modified/staged files
- Handoff chain linking
Usage:
python create_handoff.py [task-slug] [--continues-from <previous-handoff>]
python create_handoff.py "implementing-auth"
python create_handoff.py "auth-part-2" --continues-from 2024-01-15-auth.md
python create_handoff.py # auto-generates slug from timestamp
"""
import argparse
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
def run_cmd(cmd: list[str], cwd: str = None) -> tuple[bool, str]:
"""Run a command and return (success, output)."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=cwd,
timeout=10
)
return result.returncode == 0, result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return False, ""
def get_git_info(project_path: str) -> dict:
"""Gather git information from the project."""
info = {
"is_git_repo": False,
"branch": None,
"recent_commits": [],
"modified_files": [],
"staged_files": [],
}
# Check if git repo
success, _ = run_cmd(["git", "rev-parse", "--git-dir"], cwd=project_path)
if not success:
return info
info["is_git_repo"] = True
# Get current branch
success, branch = run_cmd(["git", "branch", "--show-current"], cwd=project_path)
if success and branch:
info["branch"] = branch
# Get recent commits (last 5)
success, log = run_cmd(
["git", "log", "--oneline", "-5", "--no-decorate"],
cwd=project_path
)
if success and log:
info["recent_commits"] = log.split("\n")
# Get modified files (unstaged)
success, modified = run_cmd(
["git", "diff", "--name-only"],
cwd=project_path
)
if success and modified:
info["modified_files"] = modified.split("\n")
# Get staged files
success, staged = run_cmd(
["git", "diff", "--name-only", "--cached"],
cwd=project_path
)
if success and staged:
info["staged_files"] = staged.split("\n")
return info
def find_previous_handoffs(project_path: str) -> list[dict]:
"""Find existing handoffs in the project."""
handoffs_dir = Path(project_path) / ".claude" / "handoffs"
if not handoffs_dir.exists():
return []
handoffs = []
for filepath in handoffs_dir.glob("*.md"):
# Extract title from file
try:
content = filepath.read_text()
match = re.search(r'^#\s+(?:Handoff:\s*)?(.+)$', content, re.MULTILINE)
title = match.group(1).strip() if match else filepath.stem
except Exception:
title = filepath.stem
# Parse date from filename
date_match = re.match(r'(\d{4}-\d{2}-\d{2})-(\d{6})', filepath.name)
if date_match:
try:
date = datetime.strptime(
f"{date_match.group(1)} {date_match.group(2)}",
"%Y-%m-%d %H%M%S"
)
except ValueError:
date = None
else:
date = None
handoffs.append({
"filename": filepath.name,
"path": str(filepath),
"title": title,
"date": date,
})
# Sort by date, most recent first
handoffs.sort(key=lambda x: x["date"] or datetime.min, reverse=True)
return handoffs
def get_previous_handoff_info(project_path: str, continues_from: str = None) -> dict:
"""Get information about the previous handoff for chaining."""
handoffs = find_previous_handoffs(project_path)
if continues_from:
# Find specific handoff
for h in handoffs:
if continues_from in h["filename"]:
return {
"exists": True,
"filename": h["filename"],
"title": h["title"],
}
return {"exists": False, "filename": continues_from, "title": "Not found"}
elif handoffs:
# Suggest most recent
most_recent = handoffs[0]
return {
"exists": True,
"filename": most_recent["filename"],
"title": most_recent["title"],
"suggested": True,
}
return {"exists": False}
def generate_handoff(
project_path: str,
slug: str = None,
continues_from: str = None
) -> str:
"""Generate a handoff document with pre-filled metadata."""
# Generate timestamp and filename
now = datetime.now()
timestamp = now.strftime("%Y-%m-%d %H:%M:%S")
file_timestamp = now.strftime("%Y-%m-%d-%H%M%S")
if not slug:
slug = "handoff"
# Sanitize slug
slug = slug.lower().replace(" ", "-").replace("_", "-")
slug = "".join(c for c in slug if c.isalnum() or c == "-")
filename = f"{file_timestamp}-{slug}.md"
# Create handoffs directory
handoffs_dir = Path(project_path) / ".claude" / "handoffs"
handoffs_dir.mkdir(parents=True, exist_ok=True)
filepath = handoffs_dir / filename
# Gather git info
git_info = get_git_info(project_path)
# Get previous handoff info for chaining
prev_handoff = get_previous_handoff_info(project_path, continues_from)
# Build pre-filled sections
branch_line = git_info["branch"] if git_info["branch"] else "[not a git repo or detached HEAD]"
# Recent commits section
if git_info["recent_commits"]:
commits_section = "\n".join(f" - {c}" for c in git_info["recent_commits"])
else:
commits_section = " - [no recent commits or not a git repo]"
# Modified files section
all_modified = list(set(git_info["modified_files"] + git_info["staged_files"]))
if all_modified:
modified_section = "\n".join(f"| {f} | [describe changes] | [why changed] |" for f in all_modified[:10])
if len(all_modified) > 10:
modified_section += f"\n| ... and {len(all_modified) - 10} more files | | |"
else:
modified_section = "| [no modified files detected] | | |"
# Handoff chain section
if prev_handoff.get("exists"):
chain_section = f"""## Handoff Chain
- **Continues from**: [{prev_handoff['filename']}](./{prev_handoff['filename']})
- Previous title: {prev_handoff.get('title', 'Unknown')}
- **Supersedes**: [list any older handoffs this replaces, or "None"]
> Review the previous handoff for full context before filling this one."""
else:
chain_section = """## Handoff Chain
- **Continues from**: None (fresh start)
- **Supersedes**: None
> This is the first handoff for this task."""
# Generate the document
content = f"""# Handoff: [TASK_TITLE - replace this]
## Session Metadata
- Created: {timestamp}
- Project: {project_path}
- Branch: {branch_line}
- Session duration: [estimate how long you worked]
### Recent Commits (for context)
{commits_section}
{chain_section}
## Current State Summary
[TODO: Write one paragraph describing what was being worked on, current status, and where things left off]
## Codebase Understanding
### Architecture Overview
[TODO: Document key architectural insights discovered during this session]
### Critical Files
| File | Purpose | Relevance |
|------|---------|-----------|
| [TODO: Add critical files] | | |
### Key Patterns Discovered
[TODO: Document important patterns, conventions, or idioms found in this codebase]
## Work Completed
### Tasks Finished
- [ ] [TODO: List completed tasks]
### Files Modified
| File | Changes | Rationale |
|------|---------|-----------|
{modified_section}
### Decisions Made
| Decision | Options Considered | Rationale |
|----------|-------------------|-----------|
| [TODO: Document key decisions] | | |
## Pending Work
### Immediate Next Steps
1. [TODO: Most critical next action]
2. [TODO: Second priority]
3. [TODO: Third priority]
### Blockers/Open Questions
- [ ] [TODO: List any blockers or open questions]
### Deferred Items
- [TODO: Items deferred and why]
## Context for Resuming Agent
### Important Context
[TODO: This is the MOST IMPORTANT section - write critical information the next agent MUST know]
### Assumptions Made
- [TODO: List assumptions made during this session]
### Potential Gotchas
- [TODO: Document things that might trip up a new agent]
## Environment State
### Tools/Services Used
- [TODO: List relevant tools and their configuration]
### Active Processes
- [TODO: Note any running processes, servers, etc.]
### Environment Variables
- [TODO: List relevant env var NAMES only - NEVER include actual values/secrets]
## Related Resources
- [TODO: Add links to relevant docs and files]
---
**Security Reminder**: Before finalizing, run `validate_handoff.py` to check for accidental secret exposure.
"""
# Write the file
filepath.write_text(content)
return str(filepath)
def main():
parser = argparse.ArgumentParser(
description="Create a new handoff document with smart scaffolding"
)
parser.add_argument(
"slug",
nargs="?",
default=None,
help="Short identifier for the handoff (e.g., 'implementing-auth')"
)
parser.add_argument(
"--continues-from",
dest="continues_from",
help="Filename of previous handoff this continues from"
)
args = parser.parse_args()
# Get project path (current working directory)
project_path = os.getcwd()
# Check for existing handoffs to suggest chaining
if not args.continues_from:
prev_handoffs = find_previous_handoffs(project_path)
if prev_handoffs:
print(f"Found {len(prev_handoffs)} existing handoff(s).")
print(f"Most recent: {prev_handoffs[0]['filename']}")
print(f"Use --continues-from <filename> to link handoffs.\n")
# Generate handoff
filepath = generate_handoff(project_path, args.slug, args.continues_from)
print(f"Created handoff document: {filepath}")
print(f"\nNext steps:")
print(f"1. Open {filepath}")
print(f"2. Replace [TODO: ...] placeholders with actual content")
print(f"3. Focus especially on 'Important Context' and 'Immediate Next Steps'")
print(f"4. Run: python validate_handoff.py {filepath}")
print(f" (Checks for completeness and accidental secrets)")
return filepath
if __name__ == "__main__":
main()
```
### scripts/validate_handoff.py
```python
#!/usr/bin/env python3
"""
Validate a handoff document for completeness and quality.
Checks:
- No TODO placeholders remaining
- Required sections present and populated
- No potential secrets detected
- Referenced files exist
- Quality scoring
Usage:
python validate_handoff.py <handoff-file>
python validate_handoff.py .claude/handoffs/2024-01-15-143022-auth.md
"""
import os
import re
import sys
from pathlib import Path
# Secret detection patterns
SECRET_PATTERNS = [
(r'["\']?[a-zA-Z_]*api[_-]?key["\']?\s*[:=]\s*["\'][^"\']{10,}["\']', "API key"),
(r'["\']?[a-zA-Z_]*password["\']?\s*[:=]\s*["\'][^"\']+["\']', "Password"),
(r'["\']?[a-zA-Z_]*secret["\']?\s*[:=]\s*["\'][^"\']{10,}["\']', "Secret"),
(r'["\']?[a-zA-Z_]*token["\']?\s*[:=]\s*["\'][^"\']{20,}["\']', "Token"),
(r'["\']?[a-zA-Z_]*private[_-]?key["\']?\s*[:=]', "Private key"),
(r'-----BEGIN [A-Z]+ PRIVATE KEY-----', "PEM private key"),
(r'mongodb(\+srv)?://[^/\s]+:[^@\s]+@', "MongoDB connection string with password"),
(r'postgres://[^/\s]+:[^@\s]+@', "PostgreSQL connection string with password"),
(r'mysql://[^/\s]+:[^@\s]+@', "MySQL connection string with password"),
(r'Bearer\s+[a-zA-Z0-9_\-\.]+', "Bearer token"),
(r'ghp_[a-zA-Z0-9]{36}', "GitHub personal access token"),
(r'sk-[a-zA-Z0-9]{48}', "OpenAI API key"),
(r'xox[baprs]-[a-zA-Z0-9-]+', "Slack token"),
]
# Required sections for a complete handoff
REQUIRED_SECTIONS = [
"Current State Summary",
"Important Context",
"Immediate Next Steps",
]
# Recommended sections
RECOMMENDED_SECTIONS = [
"Architecture Overview",
"Critical Files",
"Files Modified",
"Decisions Made",
"Assumptions Made",
"Potential Gotchas",
]
def check_todos(content: str) -> tuple[bool, list[str]]:
"""Check for remaining TODO placeholders."""
todos = re.findall(r'\[TODO:[^\]]*\]', content)
return len(todos) == 0, todos
def check_required_sections(content: str) -> tuple[bool, list[str]]:
"""Check that required sections exist and have content."""
missing = []
for section in REQUIRED_SECTIONS:
# Look for section header
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
match = re.search(pattern, content, re.IGNORECASE)
if not match:
missing.append(f"{section} (missing)")
else:
# Check if section has meaningful content (not just placeholder)
section_start = match.end()
next_section = re.search(r'\n##?\s+', content[section_start:])
section_end = section_start + next_section.start() if next_section else len(content)
section_content = content[section_start:section_end].strip()
# 50 chars minimum: roughly 1-2 sentences, enough to convey meaningful context
if len(section_content) < 50 or '[TODO' in section_content:
missing.append(f"{section} (incomplete)")
return len(missing) == 0, missing
def check_recommended_sections(content: str) -> list[str]:
"""Check which recommended sections are missing."""
missing = []
for section in RECOMMENDED_SECTIONS:
pattern = rf'(?:^|\n)##?\s*{re.escape(section)}'
if not re.search(pattern, content, re.IGNORECASE):
missing.append(section)
return missing
def scan_for_secrets(content: str) -> list[tuple[str, str]]:
"""Scan content for potential secrets."""
findings = []
for pattern, description in SECRET_PATTERNS:
matches = re.findall(pattern, content, re.IGNORECASE)
if matches:
findings.append((description, f"Found {len(matches)} potential match(es)"))
return findings
def check_file_references(content: str, base_path: str) -> tuple[list[str], list[str]]:
"""Check if referenced files exist."""
# Extract file paths from content (look for common patterns)
# Pattern 1: | path/to/file | in tables
# Pattern 2: `path/to/file` in code
# Pattern 3: path/to/file:123 with line numbers
patterns = [
r'\|\s*([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)\s*\|', # Table cells
r'`([a-zA-Z0-9_\-./]+\.[a-zA-Z]+(?::\d+)?)`', # Inline code
r'(?:^|\s)([a-zA-Z0-9_\-./]+\.[a-zA-Z]+:\d+)', # With line numbers
]
found_files = set()
for pattern in patterns:
matches = re.findall(pattern, content)
for match in matches:
# Remove line numbers
filepath = match.split(':')[0]
# Skip obvious non-files
if filepath and not filepath.startswith('http') and '/' in filepath:
found_files.add(filepath)
existing = []
missing = []
for filepath in found_files:
full_path = Path(base_path) / filepath
if full_path.exists():
existing.append(filepath)
else:
missing.append(filepath)
return existing, missing
def calculate_quality_score(
todos_clear: bool,
required_complete: bool,
missing_required: list,
missing_recommended: list,
secrets_found: list,
files_missing: list
) -> tuple[int, str]:
"""Calculate overall quality score (0-100).
Scoring rationale:
- Start at 100, deduct for issues
- TODOs remaining (-30): Indicates incomplete work, major blocker
- Missing required sections (-10 each): Core context gaps
- Secrets detected (-20): Security risk, must be fixed
- Missing file refs (-5 each, max -20): Stale references
- Missing recommended (-2 each): Nice-to-have completeness
"""
score = 100
# Deductions with justifications
if not todos_clear:
# -30: TODOs indicate unfinished work; next agent will lack critical info
score -= 30
if not required_complete:
# -10 per section: Required sections are essential for handoff continuity
score -= 10 * len(missing_required)
if secrets_found:
# -20: Security risk; handoffs may be shared or stored in repos
score -= 20
if files_missing:
# -5 per file (max 4): Indicates stale refs; cap at -20 to avoid over-penalizing
score -= 5 * min(len(files_missing), 4)
# -2 per section: Recommended but not critical; minor impact on handoff quality
score -= 2 * len(missing_recommended)
score = max(0, score)
# Rating thresholds based on handoff usability:
# 90+: Comprehensive, ready to use immediately
# 70-89: Usable with minor gaps
# 50-69: Needs work before reliable handoff
# <50: Too incomplete to be useful
if score >= 90:
rating = "Excellent - Ready for handoff"
elif score >= 70:
rating = "Good - Minor improvements suggested"
elif score >= 50:
rating = "Fair - Needs attention before handoff"
else:
rating = "Poor - Significant work needed"
return score, rating
def validate_handoff(filepath: str) -> dict:
"""Run all validations on a handoff file."""
path = Path(filepath)
if not path.exists():
return {"error": f"File not found: {filepath}"}
content = path.read_text()
base_path = path.parent.parent.parent # Go up from .claude/handoffs/
# Run checks
todos_clear, remaining_todos = check_todos(content)
required_complete, missing_required = check_required_sections(content)
missing_recommended = check_recommended_sections(content)
secrets_found = scan_for_secrets(content)
existing_files, missing_files = check_file_references(content, str(base_path))
# Calculate score
score, rating = calculate_quality_score(
todos_clear, required_complete, missing_required,
missing_recommended, secrets_found, missing_files
)
return {
"filepath": str(path),
"score": score,
"rating": rating,
"todos_clear": todos_clear,
"remaining_todos": remaining_todos[:5], # Limit output
"todo_count": len(remaining_todos) if not todos_clear else 0,
"required_complete": required_complete,
"missing_required": missing_required,
"missing_recommended": missing_recommended,
"secrets_found": secrets_found,
"files_verified": len(existing_files),
"files_missing": missing_files[:5], # Limit output
}
def print_report(result: dict):
"""Print a formatted validation report."""
if "error" in result:
print(f"Error: {result['error']}")
return False
print(f"\n{'='*60}")
print(f"Handoff Validation Report")
print(f"{'='*60}")
print(f"File: {result['filepath']}")
print(f"\nQuality Score: {result['score']}/100 - {result['rating']}")
print(f"{'='*60}")
# TODOs
if result['todos_clear']:
print("\n[PASS] No TODO placeholders remaining")
else:
print(f"\n[FAIL] {result['todo_count']} TODO placeholders found:")
for todo in result['remaining_todos']:
print(f" - {todo[:50]}...")
# Required sections
if result['required_complete']:
print("\n[PASS] All required sections complete")
else:
print("\n[FAIL] Missing/incomplete required sections:")
for section in result['missing_required']:
print(f" - {section}")
# Secrets
if not result['secrets_found']:
print("\n[PASS] No potential secrets detected")
else:
print("\n[WARN] Potential secrets detected:")
for secret_type, detail in result['secrets_found']:
print(f" - {secret_type}: {detail}")
# File references
if result['files_missing']:
print(f"\n[WARN] {len(result['files_missing'])} referenced file(s) not found:")
for f in result['files_missing']:
print(f" - {f}")
else:
print(f"\n[INFO] {result['files_verified']} file reference(s) verified")
# Recommended sections
if result['missing_recommended']:
print(f"\n[INFO] Consider adding these sections:")
for section in result['missing_recommended']:
print(f" - {section}")
print(f"\n{'='*60}")
# Final verdict
if result['score'] >= 70 and not result['secrets_found']:
print("Verdict: READY for handoff")
return True
elif result['secrets_found']:
print("Verdict: BLOCKED - Remove secrets before handoff")
return False
else:
print("Verdict: NEEDS WORK - Complete required sections")
return False
def main():
if len(sys.argv) < 2:
print("Usage: python validate_handoff.py <handoff-file>")
print("Example: python validate_handoff.py .claude/handoffs/2024-01-15-auth.md")
sys.exit(1)
filepath = sys.argv[1]
result = validate_handoff(filepath)
success = print_report(result)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
```
### scripts/list_handoffs.py
```python
#!/usr/bin/env python3
"""
List available handoff documents in the current project.
Searches for handoff documents in .claude/handoffs/ and displays:
- Filename with date
- Title extracted from document
- Status (if marked complete)
Usage:
python list_handoffs.py # List handoffs in current project
python list_handoffs.py /path # List handoffs in specified path
"""
import os
import re
import sys
from datetime import datetime
from pathlib import Path
def extract_title(filepath: Path) -> str:
"""Extract the title from a handoff document."""
try:
content = filepath.read_text()
# Look for first H1 header
match = re.search(r'^#\s+(?:Handoff:\s*)?(.+)$', content, re.MULTILINE)
if match:
title = match.group(1).strip()
# Clean up placeholder text
if title.startswith("[") and title.endswith("]"):
return "[Untitled - needs completion]"
return title[:50] + "..." if len(title) > 50 else title
except Exception:
pass
return "[Unable to read title]"
def check_completion_status(filepath: Path) -> str:
"""Check if handoff appears complete or has TODOs remaining."""
try:
content = filepath.read_text()
todo_count = content.count("[TODO:")
if todo_count == 0:
return "Complete"
elif todo_count <= 3:
return f"In Progress ({todo_count} TODOs)"
else:
return f"Needs Work ({todo_count} TODOs)"
except Exception:
return "Unknown"
def parse_date_from_filename(filename: str) -> datetime | None:
"""Extract date from filename like 2024-01-15-143022-slug.md"""
match = re.match(r'(\d{4}-\d{2}-\d{2})-(\d{6})', filename)
if match:
try:
date_str = match.group(1)
time_str = match.group(2)
return datetime.strptime(f"{date_str} {time_str}", "%Y-%m-%d %H%M%S")
except ValueError:
pass
return None
def list_handoffs(project_path: str) -> list[dict]:
"""List all handoff documents in a project."""
handoffs_dir = Path(project_path) / ".claude" / "handoffs"
if not handoffs_dir.exists():
return []
handoffs = []
for filepath in handoffs_dir.glob("*.md"):
parsed_date = parse_date_from_filename(filepath.name)
handoffs.append({
"path": str(filepath),
"filename": filepath.name,
"title": extract_title(filepath),
"status": check_completion_status(filepath),
"date": parsed_date,
"size": filepath.stat().st_size,
})
# Sort by date, most recent first
handoffs.sort(key=lambda x: x["date"] or datetime.min, reverse=True)
return handoffs
def format_date(dt: datetime | None) -> str:
"""Format datetime for display."""
if dt is None:
return "Unknown date"
return dt.strftime("%Y-%m-%d %H:%M")
def main():
# Get project path
project_path = sys.argv[1] if len(sys.argv) > 1 else os.getcwd()
handoffs = list_handoffs(project_path)
if not handoffs:
print(f"No handoffs found in {project_path}/.claude/handoffs/")
print("\nTo create a handoff, run: python create_handoff.py [task-slug]")
return
print(f"Found {len(handoffs)} handoff(s) in {project_path}/.claude/handoffs/\n")
print("-" * 80)
for h in handoffs:
print(f" Date: {format_date(h['date'])}")
print(f" Title: {h['title']}")
print(f" Status: {h['status']}")
print(f" File: {h['filename']}")
print("-" * 80)
print(f"\nTo resume from a handoff, read the document and follow the resume checklist.")
print(f"Most recent: {handoffs[0]['path']}")
if __name__ == "__main__":
main()
```
### scripts/check_staleness.py
```python
#!/usr/bin/env python3
"""
Check staleness of a handoff document compared to current project state.
Analyzes:
- Time since handoff was created
- Git commits since handoff
- Files that changed since handoff
- Branch divergence
- Modified files status
Usage:
python check_staleness.py <handoff-file>
python check_staleness.py .claude/handoffs/2024-01-15-143022-auth.md
"""
import os
import re
import subprocess
import sys
from datetime import datetime
from pathlib import Path
def run_cmd(cmd: list[str], cwd: str = None) -> tuple[bool, str]:
"""Run a command and return (success, output)."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=cwd,
timeout=10
)
return result.returncode == 0, result.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError):
return False, ""
def parse_handoff_metadata(filepath: str) -> dict:
"""Extract metadata from a handoff file."""
content = Path(filepath).read_text()
metadata = {
"created": None,
"branch": None,
"project_path": None,
"modified_files": [],
}
# Parse Created timestamp
match = re.search(r'Created:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})', content)
if match:
try:
metadata["created"] = datetime.strptime(match.group(1), "%Y-%m-%d %H:%M:%S")
except ValueError:
pass
# Parse Branch
match = re.search(r'Branch:\s*(\S+)', content)
if match:
branch = match.group(1)
if branch and not branch.startswith('['):
metadata["branch"] = branch
# Parse Project path
match = re.search(r'Project:\s*(.+?)(?:\n|$)', content)
if match:
metadata["project_path"] = match.group(1).strip()
# Parse modified files from table
table_matches = re.findall(r'\|\s*([a-zA-Z0-9_\-./]+\.[a-zA-Z]+)\s*\|', content)
for f in table_matches:
if '/' in f and not f.startswith('['):
metadata["modified_files"].append(f)
return metadata
def get_commits_since(timestamp: datetime, project_path: str) -> list[str]:
"""Get list of commits since a given timestamp."""
if not timestamp:
return []
iso_time = timestamp.strftime("%Y-%m-%dT%H:%M:%S")
success, output = run_cmd(
["git", "log", f"--since={iso_time}", "--oneline", "--no-decorate"],
cwd=project_path
)
if success and output:
return output.split("\n")
return []
def get_current_branch(project_path: str) -> str | None:
"""Get current git branch."""
success, branch = run_cmd(
["git", "branch", "--show-current"],
cwd=project_path
)
return branch if success else None
def get_changed_files_since(timestamp: datetime, project_path: str) -> list[str]:
"""Get files that changed since timestamp."""
if not timestamp:
return []
iso_time = timestamp.strftime("%Y-%m-%dT%H:%M:%S")
success, output = run_cmd(
["git", "diff", "--name-only", f"--since={iso_time}", "HEAD"],
cwd=project_path
)
# Fallback: get files changed in commits since timestamp
if not output:
success, output = run_cmd(
["git", "log", f"--since={iso_time}", "--name-only", "--pretty=format:"],
cwd=project_path
)
if success and output:
files = [f.strip() for f in output.split("\n") if f.strip()]
return list(set(files)) # Deduplicate
return []
def check_files_exist(files: list[str], project_path: str) -> tuple[list[str], list[str]]:
"""Check which files from handoff still exist."""
existing = []
missing = []
for f in files:
full_path = Path(project_path) / f
if full_path.exists():
existing.append(f)
else:
missing.append(f)
return existing, missing
def calculate_staleness_level(
days_old: float,
commits_since: int,
files_changed: int,
branch_matches: bool,
files_missing: int
) -> tuple[str, str, list[str]]:
"""Calculate staleness level and provide recommendations.
Staleness scoring rationale:
- Each factor adds 1-3 points based on severity
- Thresholds based on typical development patterns:
- Age: 1 day (active work), 7 days (sprint), 30 days (stale)
- Commits: 5 (minor changes), 20 (feature work), 50 (major changes)
- Files: 5 (localized), 20 (widespread changes)
- Final score: 0=FRESH, 1-2=SLIGHTLY_STALE, 3-4=STALE, 5+=VERY_STALE
"""
issues = []
# Scoring
staleness_score = 0
# Age thresholds: 1 day = active, 7 days = sprint boundary, 30 days = likely stale
if days_old > 30:
staleness_score += 3 # Over a month: high risk of outdated context
issues.append(f"Handoff is {int(days_old)} days old")
elif days_old > 7:
staleness_score += 2 # Over a week: moderate staleness
issues.append(f"Handoff is {int(days_old)} days old")
elif days_old > 1:
staleness_score += 1 # Over a day: minor staleness
# Commit thresholds: 5 = routine, 20 = feature work, 50 = major development
if commits_since > 50:
staleness_score += 3 # Major changes likely invalidate handoff context
issues.append(f"{commits_since} commits since handoff - significant changes")
elif commits_since > 20:
staleness_score += 2 # Substantial work done since handoff
issues.append(f"{commits_since} commits since handoff")
elif commits_since > 5:
staleness_score += 1 # Some changes, worth reviewing
# Branch mismatch: likely working on different feature/context
if not branch_matches:
staleness_score += 2 # Different branch = different context
issues.append("Current branch differs from handoff branch")
# Missing files: 5+ suggests significant restructuring
if files_missing > 5:
staleness_score += 2 # Many refs broken = codebase restructured
issues.append(f"{files_missing} referenced files no longer exist")
elif files_missing > 0:
staleness_score += 1 # Some refs broken
issues.append(f"{files_missing} referenced file(s) missing")
# Changed files: 5 = localized, 20 = widespread
if files_changed > 20:
staleness_score += 2 # Widespread changes affect handoff relevance
issues.append(f"{files_changed} files changed since handoff")
elif files_changed > 5:
staleness_score += 1 # Some files changed
# Staleness levels: 0=fresh, 1-2=slight, 3-4=stale, 5+=very stale
if staleness_score == 0:
level = "FRESH"
recommendation = "Safe to resume - minimal changes since handoff"
elif staleness_score <= 2:
level = "SLIGHTLY_STALE"
recommendation = "Generally safe to resume - review changes before continuing"
elif staleness_score <= 4:
level = "STALE"
recommendation = "Proceed with caution - significant changes may affect context"
else:
level = "VERY_STALE"
recommendation = "Consider creating new handoff - too many changes since original"
return level, recommendation, issues
def check_staleness(handoff_path: str) -> dict:
"""Run staleness check on a handoff file."""
path = Path(handoff_path)
if not path.exists():
return {"error": f"Handoff file not found: {handoff_path}"}
# Parse handoff
metadata = parse_handoff_metadata(handoff_path)
# Determine project path
project_path = metadata.get("project_path")
if not project_path or not Path(project_path).exists():
# Fallback: assume handoff is in .claude/handoffs/ within project
project_path = str(path.parent.parent.parent)
# Check if git repo
success, _ = run_cmd(["git", "rev-parse", "--git-dir"], cwd=project_path)
is_git_repo = success
result = {
"handoff_file": str(path),
"project_path": project_path,
"is_git_repo": is_git_repo,
"created": metadata["created"],
"handoff_branch": metadata["branch"],
}
# Calculate age
if metadata["created"]:
age = datetime.now() - metadata["created"]
result["days_old"] = age.total_seconds() / 86400
result["hours_old"] = age.total_seconds() / 3600
else:
result["days_old"] = None
result["hours_old"] = None
if is_git_repo:
# Git-based checks
result["current_branch"] = get_current_branch(project_path)
result["branch_matches"] = (
result["current_branch"] == metadata["branch"]
if metadata["branch"] else True
)
commits = get_commits_since(metadata["created"], project_path)
result["commits_since"] = len(commits)
result["recent_commits"] = commits[:5] # Show first 5
changed_files = get_changed_files_since(metadata["created"], project_path)
result["files_changed_count"] = len(changed_files)
result["files_changed"] = changed_files[:10] # Show first 10
# Check if handoff's modified files still exist
existing, missing = check_files_exist(metadata["modified_files"], project_path)
result["referenced_files_exist"] = len(existing)
result["referenced_files_missing"] = missing
# Calculate staleness
level, recommendation, issues = calculate_staleness_level(
result.get("days_old", 0) or 0,
result["commits_since"],
result["files_changed_count"],
result["branch_matches"],
len(missing)
)
result["staleness_level"] = level
result["recommendation"] = recommendation
result["issues"] = issues
else:
# Non-git checks (limited)
result["staleness_level"] = "UNKNOWN"
result["recommendation"] = "Not a git repo - unable to detect changes"
result["issues"] = ["Project is not a git repository"]
return result
def print_report(result: dict):
"""Print staleness report."""
if "error" in result:
print(f"Error: {result['error']}")
return
print(f"\n{'='*60}")
print(f"Handoff Staleness Report")
print(f"{'='*60}")
print(f"File: {result['handoff_file']}")
print(f"Project: {result['project_path']}")
if result["created"]:
print(f"Created: {result['created'].strftime('%Y-%m-%d %H:%M:%S')}")
if result["days_old"] is not None:
if result["days_old"] < 1:
print(f"Age: {result['hours_old']:.1f} hours")
else:
print(f"Age: {result['days_old']:.1f} days")
print(f"\n{'='*60}")
print(f"Staleness Level: {result['staleness_level']}")
print(f"{'='*60}")
print(f"\nRecommendation: {result['recommendation']}")
if result.get("issues"):
print(f"\nIssues detected:")
for issue in result["issues"]:
print(f" - {issue}")
if result.get("is_git_repo"):
print(f"\n--- Git Status ---")
print(f"Handoff branch: {result.get('handoff_branch', 'Unknown')}")
print(f"Current branch: {result.get('current_branch', 'Unknown')}")
print(f"Branch matches: {'Yes' if result.get('branch_matches') else 'No'}")
print(f"Commits since handoff: {result.get('commits_since', 0)}")
print(f"Files changed: {result.get('files_changed_count', 0)}")
if result.get("recent_commits"):
print(f"\nRecent commits:")
for commit in result["recent_commits"][:5]:
print(f" {commit}")
if result.get("referenced_files_missing"):
print(f"\nMissing referenced files:")
for f in result["referenced_files_missing"][:5]:
print(f" - {f}")
print(f"\n{'='*60}")
# Color-coded verdict (using text indicators)
level = result.get("staleness_level", "UNKNOWN")
if level == "FRESH":
print("Verdict: [OK] Safe to resume")
elif level == "SLIGHTLY_STALE":
print("Verdict: [OK] Review changes, then resume")
elif level == "STALE":
print("Verdict: [CAUTION] Verify context before resuming")
elif level == "VERY_STALE":
print("Verdict: [WARNING] Consider creating fresh handoff")
else:
print("Verdict: [UNKNOWN] Manual verification needed")
def main():
if len(sys.argv) < 2:
print("Usage: python check_staleness.py <handoff-file>")
print("Example: python check_staleness.py .claude/handoffs/2024-01-15-auth.md")
sys.exit(1)
handoff_path = sys.argv[1]
result = check_staleness(handoff_path)
print_report(result)
# Exit code based on staleness
level = result.get("staleness_level", "UNKNOWN")
if level in ["FRESH", "SLIGHTLY_STALE"]:
sys.exit(0)
elif level == "STALE":
sys.exit(1)
else:
sys.exit(2)
if __name__ == "__main__":
main()
```