Skill: wpank/session-handoff (version 0.1.0)

--- name: session-handoff model: standard description: | WHAT: Create comprehensive handoff documents that enable fresh AI agents to seamlessly continue work with zero ambiguity. Solves long-running agent context exhaustion problem. WHEN: (1) User requests handoff/memory/context save, (2) Context window approaches capacity, (3) Major task milestone completed, (4) Work session ending, (5) Resuming work with existing handoff. KEYWORDS: "save state", "create handoff", "context is full", "I need to pause", "resume from", "continue where we left off", "load handoff", "save progress", "session transfer", "hand off" --- # Session Handoff Create handoff documents that enable fresh agents to continue work seamlessly. ## Mode Selection **Creating a handoff?** User wants to save state, pause work, or context is full. → Follow CREATE Workflow **Resuming from a handoff?** User wants to continue previous work or load context. → Follow RESUME Workflow **Proactive suggestion?** After substantial work (5+ file edits, complex debugging, major decisions): > "Consider creating a handoff document to preserve this context. Say 'create handoff' when ready." --- ## CREATE Workflow ### Step 1: Generate Scaffold Run the smart scaffold script: ```bash python scripts/create_handoff.py [task-slug] ``` 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 creates `.claude/handoffs/` directory and generates a timestamped file with pre-filled metadata (timestamp, project path, git branch, recent commits, modified files). ### Step 2: Complete the Document Open the generated file and fill all `[TODO: ...]` sections. Prioritize: 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) See [references/handoff-template.md](references/handoff-template.md) for full structure. ### Step 3: Validate ```bash python scripts/validate_handoff.py <handoff-file> ``` 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 handoffs with secrets detected or score below 70.** ### Step 4: Confirm Report to user: - Handoff file location - Validation score and warnings - Summary of captured context - First action item for next session --- ## RESUME Workflow ### Step 1: Find Handoffs ```bash python scripts/list_handoffs.py ``` ### Step 2: Check Staleness ```bash python scripts/check_staleness.py <handoff-file> ``` Staleness levels: - **FRESH**: Safe to resume - **SLIGHTLY_STALE**: Review changes first - **STALE**: Verify context carefully - **VERY_STALE**: Consider creating fresh handoff ### Step 3: Load and Verify Read the handoff document completely. If part of a chain, also read the previous handoff. Follow [references/resume-checklist.md](references/resume-checklist.md): 1. Verify project directory and git branch match 2. Check if blockers resolved 3. Validate assumptions still hold 4. Review modified files for conflicts ### Step 4: Begin Work Start with "Immediate Next Steps" item #1. Reference as you work: - "Critical Files" for important locations - "Key Patterns Discovered" for conventions - "Potential Gotchas" to avoid known issues --- ## Handoff Chaining For long-running projects, chain handoffs 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 ``` When resuming from a chain, read the most recent handoff first, then reference predecessors as needed. --- ## Storage Location: `.claude/handoffs/` Naming: `YYYY-MM-DD-HHMMSS-[slug].md` --- ## Quality Criteria Good handoffs have: - Zero ambiguity about current state - Clear, numbered next steps - Rationale for decisions (not just outcomes) - File paths with line numbers where relevant - No secrets or credentials --- ## NEVER - Include API keys, passwords, tokens, or credentials - Leave TODO placeholders in finalized handoffs - Skip the validation step - Create handoffs without the Important Context section - Finalize handoffs with quality score below 70

path: README.md
content: # Session Handoff

Create comprehensive handoff documents that enable fresh AI agents to seamlessly continue work with zero ambiguity. Solves the long-running agent context exhaustion problem.

## What's Inside

- CREATE workflow (scaffold, complete, validate, confirm)
- RESUME workflow (find handoffs, check staleness, load and verify, begin work)
- Handoff chaining for long-running projects
- Validation scripts with quality scoring (0-100)
- Staleness detection (FRESH, SLIGHTLY_STALE, STALE, VERY_STALE)
- Handoff template with required sections
- Resume checklist for picking up previous work

## When to Use

- User requests handoff, memory, or context save
- Context window approaches capacity
- Major task milestone completed
- Work session ending
- Resuming work with an existing handoff
- Triggered by: "save state", "create handoff", "context is full", "I need to pause", "resume from", "continue where we left off", "load handoff", "save progress"

## Installation

```bash
npx add https://github.com/wpank/ai/tree/main/skills/tools/session-handoff
```

### Manual Installation

#### Cursor (per-project)

From your project root:

```bash
mkdir -p .cursor/skills
cp -r ~/.ai-skills/skills/tools/session-handoff .cursor/skills/session-handoff
```

#### Cursor (global)

```bash
mkdir -p ~/.cursor/skills
cp -r ~/.ai-skills/skills/tools/session-handoff ~/.cursor/skills/session-handoff
```

#### Claude Code (per-project)

From your project root:

```bash
mkdir -p .claude/skills
cp -r ~/.ai-skills/skills/tools/session-handoff .claude/skills/session-handoff
```

#### Claude Code (global)

```bash
mkdir -p ~/.claude/skills
cp -r ~/.ai-skills/skills/tools/session-handoff ~/.claude/skills/session-handoff
```

---

Part of the [Tools](..) skill category.


path: references/handoff-template.md
content: # 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


path: references/resume-checklist.md
content: # 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.


path: scripts/check_staleness.py
content: #!/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()


path: scripts/create_handoff.py
content: #!/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()


path: scripts/list_handoffs.py
content: #!/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()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)