Commit d78d542bf6ba

Vincent Demeester <vincent@sbr.pm>
2026-04-15 16:17:16
feat: add DocCoauthoring, DocsDriftReview, Handoff skills
Added three medium-priority skills: DocCoauthoring for structured co-authoring of docs/RFCs/proposals, DocsDriftReview for auditing documentation against implementation, and Handoff for cross-agent context transfer. Removed handoff.ts extension which forced new session creation, replaced by Handoff skill with persistent handoff documents. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Vincent Demeester <vincent@sbr.pm>
1 parent 4547f55
Changed files (8)
dots
config
pi
agent
extensions
dots/config/claude/skills/DocCoauthoring/SKILL.md
@@ -0,0 +1,98 @@
+---
+name: DocCoauthoring
+description: Guide users through structured co-authoring of documentation, proposals, specs, and written artifacts. USE WHEN user wants to write a design doc, RFC, proposal, technical spec, KEP, enhancement proposal, or co-author documentation.
+---
+
+# Doc Co-Authoring
+
+Turn incomplete context into a clear document through an interactive, structured workflow. Emphasis on concise, idiomatic English and incremental validation.
+
+## When To Use
+
+- Write a design doc, RFC, proposal, PRD, decision record, or technical spec
+- Restructure or refine an existing document
+- Copyedit a draft for grammar, clarity, or natural English
+- Rewrite awkward or non-native phrasing
+- Gather context and turn it into a coherent written artifact
+- Red Hat-specific: KEP, enhancement proposals, release notes
+
+Do not force this workflow. If the user wants freeform help, use a lighter touch.
+
+## Workflow
+
+### 1. Align on the Document
+
+Ask for minimum context needed:
+- Document type and template (if any)
+- Primary audience
+- Desired outcome from readers
+- Deadlines, approvals, or constraints
+
+If a draft already exists, read it before asking questions.
+
+### 2. Gather Context
+
+Accept context in any form: rough info dump, links, notes, constraints, tradeoffs, rejected alternatives. Then ask focused follow-up questions to close the biggest gaps. Prefer a small number of high-value questions over a long interview.
+
+### 3. Agree on Structure
+
+- Identify target sections
+- Suggest structure if user doesn't have one
+- Start with the section containing the main decision or the most unknowns
+
+### 4. Draft Section by Section
+
+For each section:
+1. Ask a few clarifying questions specific to that section
+2. Brainstorm candidate points
+3. Ask what to keep, combine, or drop
+4. Draft the section
+5. Refine based on targeted feedback
+
+Prefer surgical edits and short iteration loops. Avoid rewriting the entire document when only one section needs work.
+
+### 5. Review the Full Document
+
+Reread and check for:
+- Gaps in logic
+- Duplicated content
+- Contradictions between sections
+- Generic filler that can be removed
+- Missing context readers actually need
+
+### 6. Reader Test
+
+Pressure-test from a new reader's perspective:
+- List key questions a reader should be able to answer after reading
+- Check whether the draft answers them clearly
+- Identify hidden assumptions, undefined terms, ambiguous statements
+
+## Language Pass
+
+When improving English:
+- Rewrite awkward or literal phrasing into natural English
+- Prefer short, direct sentences over long or tangled ones
+- Use active voice unless passive is clearer
+- Fix articles, tense, agreement, punctuation, parallel structure
+- Remove redundancy, filler, repeated words
+- Keep terminology consistent
+
+Examples:
+- ❌ "This feature allows to users configure the runner easily."
+- ✅ "This feature lets users configure the runner easily."
+- ❌ "We made this change for avoid failures during deploy."
+- ✅ "We made this change to avoid deployment failures."
+
+## Editing Guidelines
+
+When making direct edits:
+- Fix clarity, grammar, and natural English first
+- Keep the existing voice unless asked to change tone
+- Preserve useful content, tighten weak content
+- Prefer concrete examples over abstract filler
+- Avoid making policy decisions the user hasn't made
+
+## Related Skills
+
+- **Brainstorming**: For exploring ideas and approaches before writing
+- **WritingPlans**: For implementation plans (code-focused, not doc-focused)
dots/config/claude/skills/DocsDriftReview/scripts/collect-code-files.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+find . \
+  -type f \
+  \( -iname "*.go" -o -iname "*.py" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.js" -o -iname "*.jsx" -o -iname "*.java" -o -iname "*.rs" -o -iname "*.yaml" -o -iname "*.yml" -o -iname "*.json" -o -iname "*.proto" \) |
+  sort
dots/config/claude/skills/DocsDriftReview/scripts/collect-doc-files.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+find . \
+  -type f \
+  \( -iname "*.md" -o -iname "*.mdx" -o -iname "*.rst" -o -iname "*.adoc" -o -iname "README*" \) |
+  sort
dots/config/claude/skills/DocsDriftReview/templates/report-template.md
@@ -0,0 +1,39 @@
+# Documentation Drift Review
+
+## Summary
+
+- Overall assessment:
+- Critical:
+- High:
+- Medium:
+- Low:
+
+## Top issues
+
+1.
+2.
+3.
+
+## Findings
+
+### DDR-001
+
+- Severity:
+- Confidence:
+- Docs evidence:
+- Implementation evidence:
+- Why this matters:
+- Recommended fix:
+- Proposed text:
+
+## Coverage gaps
+
+-
+
+## Reorganization suggestions
+
+-
+
+## Quick wins
+
+-
dots/config/claude/skills/DocsDriftReview/SKILL.md
@@ -0,0 +1,113 @@
+---
+name: DocsDriftReview
+description: Analyze repository documentation for implementation drift, stale examples, missing coverage, and reorganization opportunities. USE WHEN user says 'review docs', 'docs drift', 'stale docs', 'doc audit', 'check docs', or wants to compare documentation against implementation.
+---
+
+# Docs Drift Review
+
+Audit documentation against the actual implementation. Find stale content, missing coverage, and correctness issues. Produce a prioritized report with exact file references and proposed fixes.
+
+## Non-Goals
+
+- Don't invent behavior not supported by code or tests
+- Don't rewrite all docs when a targeted patch is enough
+- Don't flag style preferences as defects unless they reduce clarity
+- Don't modify files unless the user explicitly asks for edits
+
+## Workflow
+
+### Step 1: Build a Docs Map
+
+Create a compact inventory:
+- Major doc files and their purpose
+- Feature areas covered
+- Source-of-truth files for each area
+- Stale-looking sections (versioned commands, flags, env vars, API paths, copied outputs)
+
+### Step 2: Build an Implementation Map
+
+Identify:
+- Entrypoints, public interfaces
+- Commands, flags, config keys, env vars
+- API routes, request/response shapes
+- Feature gates, defaults, constraints
+- Examples, fixtures, relevant tests
+
+Prefer tests and schemas over comments when determining current behavior.
+
+### Step 3: Compare Docs Against Implementation
+
+Check for:
+- Renamed or removed commands, flags, env vars, config fields, APIs
+- Changed defaults or prerequisites
+- Outdated examples or sample outputs
+- Undocumented new behavior
+- Docs claiming support that code no longer provides
+- Missing constraints, edge cases, or failure modes
+
+For each suspected drift: find exact evidence in docs AND in code/tests. Classify as **confirmed**, **likely**, or **insufficient evidence**.
+
+### Step 4: Review Doc Quality
+
+Check for:
+- Spelling and grammar errors
+- Inconsistent naming of products, features, commands
+- Ambiguous wording, duplicate content
+- Examples without explanation or explanation without examples
+- Missing cross-links, outdated file path references
+
+### Step 5: Produce Prioritized Report
+
+## Severity Guide
+
+| Level | Criteria |
+|-------|----------|
+| **critical** | Setup-breaking, security-relevant, migration-breaking, or API-breaking doc errors |
+| **high** | Materially misleading docs that waste time or cause wrong usage |
+| **medium** | Missing context, stale examples, structural issues |
+| **low** | Spelling, grammar, naming consistency, polish |
+
+## Output Format
+
+```markdown
+### Summary
+- Overall docs health
+- Count of findings by severity
+- Top 3 issues
+
+### Findings
+For each finding:
+- ID, Severity, Confidence (confirmed/likely/uncertain)
+- Docs evidence (file:line)
+- Implementation evidence (file:line)
+- Why this matters
+- Recommended fix
+
+### Coverage Gaps
+Feature areas implemented but not documented.
+
+### Quick Wins
+Smallest high-value doc fixes.
+```
+
+## Evidence Rules
+
+- Prefer direct evidence from code, tests, schemas, generated help
+- If behavior is ambiguous, say so explicitly
+- Don't infer runtime behavior solely from type names or comments
+- If tests contradict docs, treat as drift signal and note the contradiction
+
+## Common Drift Patterns
+
+- README says one command, CLI help exposes another
+- Docs list flags not in parser definitions
+- Docs omit required env vars now enforced by code
+- Examples use old API paths or field names
+- Docs describe defaults that changed in config
+- Docs refer to moved files or directories
+- Generated CRDs changed but prose docs didn't
+
+## Related Skills
+
+- **CodeReview**: For code quality review (not docs)
+- **DocCoauthoring**: For writing new documentation
dots/config/claude/skills/Handoff/scripts/create_handoff.py
@@ -0,0 +1,350 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import json
+import random
+import re
+import subprocess
+import sys
+from pathlib import Path
+from typing import Any, Sequence
+
+SCAFFOLD_TEMPLATE = """\
+# Handoff: {name}
+
+## Goal
+
+## Current Progress
+
+## What Worked
+
+## What Didn't Work
+
+## Next Steps
+
+## Notes for Next Agent
+"""
+
+HANDOFF_DIR = Path.home() / ".local" / "share" / "ai" / "handoffs"
+
+
+def run_git(args: Sequence[str], cwd: Path) -> subprocess.CompletedProcess[str]:
+    return subprocess.run(
+        ["git", *args],
+        cwd=cwd,
+        text=True,
+        capture_output=True,
+    )
+
+
+def parse_args() -> argparse.Namespace:
+    parser = argparse.ArgumentParser(
+        description="Gather git context and scaffold a handoff document.",
+        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+    )
+    parser.add_argument(
+        "--repo", default=".", help="Path inside the target Git repository."
+    )
+    parser.add_argument(
+        "--branch",
+        default=None,
+        help="Branch name override (auto-detected by default).",
+    )
+    parser.add_argument(
+        "--name",
+        required=True,
+        help="Descriptive kebab-case slug for the handoff file (e.g. 'fix-auth-token-expiry').",
+    )
+    parser.add_argument(
+        "--json",
+        action="store_true",
+        dest="emit_json",
+        help="Emit JSON instead of text output.",
+    )
+    parser.add_argument(
+        "--log-count",
+        type=int,
+        default=10,
+        help="Number of recent commits to include.",
+    )
+    parser.add_argument(
+        "--new-copy-if-exists",
+        action="store_true",
+        help="Create a new uniquely named handoff file instead of reusing an existing slug.",
+    )
+    return parser.parse_args()
+
+
+def find_git_root(start: Path) -> Path | None:
+    result = run_git(["rev-parse", "--show-toplevel"], cwd=start)
+    if result.returncode != 0:
+        return None
+    return Path(result.stdout.strip())
+
+
+def detect_repo_name(root: Path) -> str:
+    result = run_git(["remote", "get-url", "origin"], cwd=root)
+    if result.returncode == 0:
+        url = result.stdout.strip()
+        # SSH: git@host:org/repo.git
+        m = re.search(r":([^/]+/[^/]+?)(?:\.git)?$", url)
+        if m:
+            return m.group(1).split("/")[-1]
+        # HTTPS: https://host/org/repo.git
+        m = re.search(r"/([^/]+?)(?:\.git)?$", url)
+        if m:
+            return m.group(1)
+    return root.name
+
+
+def detect_branch(root: Path) -> str | None:
+    result = run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=root)
+    if result.returncode != 0:
+        return None
+    branch = result.stdout.strip()
+    if branch == "HEAD":
+        return None
+    return branch
+
+
+def sanitize_branch(name: str) -> str:
+    return re.sub(r"[^a-zA-Z0-9_.-]", "-", name)
+
+
+def detect_default_branch(root: Path) -> str:
+    result = run_git(["symbolic-ref", "refs/remotes/origin/HEAD"], cwd=root)
+    if result.returncode == 0:
+        ref = result.stdout.strip()
+        # refs/remotes/origin/main -> main
+        parts = ref.split("/")
+        if parts:
+            return parts[-1]
+
+    # Fallback: check if main or master exists
+    for candidate in ("main", "master"):
+        result = run_git(["rev-parse", "--verify", candidate], cwd=root)
+        if result.returncode == 0:
+            return candidate
+
+    return "main"
+
+
+def ensure_directory(repo_name: str) -> Path:
+    directory = HANDOFF_DIR / repo_name
+    directory.mkdir(parents=True, exist_ok=True)
+    return directory
+
+
+def check_existing(handoff_path: Path) -> tuple[bool, str | None]:
+    if handoff_path.exists():
+        return False, handoff_path.read_text()
+    return True, None
+
+
+def generate_unique_path(handoff_path: Path) -> Path:
+    """Generate a unique path by appending a random suffix if file exists."""
+    if not handoff_path.exists():
+        return handoff_path
+
+    stem = handoff_path.stem
+    suffix = handoff_path.suffix
+    random_suffix = f"{random.randint(1000, 9999)}"
+    return handoff_path.parent / f"{stem}-{random_suffix}{suffix}"
+
+
+def gather_git_context(
+    root: Path, default_branch: str, log_count: int
+) -> dict[str, Any]:
+    context: dict[str, Any] = {}
+
+    # Recent commits
+    result = run_git(
+        ["log", f"{default_branch}..HEAD", "--oneline", f"-{log_count}"], cwd=root
+    )
+    commits = []
+    if result.returncode == 0:
+        for line in result.stdout.strip().splitlines():
+            if not line:
+                continue
+            parts = line.split(None, 1)
+            commits.append(
+                {
+                    "hash": parts[0],
+                    "message": parts[1] if len(parts) > 1 else "",
+                }
+            )
+    context["recent_commits"] = commits
+
+    # Files changed
+    result = run_git(["diff", "--name-status", f"{default_branch}...HEAD"], cwd=root)
+    files_changed = []
+    if result.returncode == 0:
+        for line in result.stdout.strip().splitlines():
+            if not line:
+                continue
+            parts = line.split(None, 1)
+            files_changed.append(
+                {
+                    "status": parts[0],
+                    "path": parts[1] if len(parts) > 1 else "",
+                }
+            )
+    context["files_changed"] = files_changed
+
+    # Ahead/behind
+    result = run_git(
+        ["rev-list", "--left-right", "--count", f"{default_branch}...HEAD"], cwd=root
+    )
+    if result.returncode == 0:
+        parts = result.stdout.strip().split()
+        if len(parts) == 2:
+            context["behind"] = int(parts[0])
+            context["ahead"] = int(parts[1])
+        else:
+            context["ahead"] = 0
+            context["behind"] = 0
+    else:
+        context["ahead"] = 0
+        context["behind"] = 0
+
+    # Working tree
+    result = run_git(["status", "--porcelain"], cwd=root)
+    modified = 0
+    untracked = 0
+    staged = 0
+    if result.returncode == 0:
+        for line in result.stdout.splitlines():
+            if not line or len(line) < 2:
+                continue
+            x, y = line[0], line[1]
+            if x == "?":
+                untracked += 1
+            elif x != " ":
+                staged += 1
+            if y not in (" ", "?"):
+                modified += 1
+    context["working_tree"] = {
+        "modified": modified,
+        "untracked": untracked,
+        "staged": staged,
+    }
+
+    return context
+
+
+def scaffold_template(handoff_path: Path, name: str) -> None:
+    content = SCAFFOLD_TEMPLATE.format(name=name)
+    handoff_path.write_text(content)
+
+
+def main() -> int:
+    args = parse_args()
+    start = Path(args.repo).resolve()
+
+    root = find_git_root(start)
+    if root is None:
+        print("Error: not inside a Git repository.", file=sys.stderr)
+        return 1
+
+    repo_name = detect_repo_name(root)
+
+    branch = args.branch or detect_branch(root)
+    if branch is None:
+        branch = "(detached)"
+
+    sanitized = sanitize_branch(args.name)
+    default_branch = detect_default_branch(root)
+
+    handoff_dir = ensure_directory(repo_name)
+    preferred_path = handoff_dir / f"{sanitized}.md"
+
+    is_new, existing_content = check_existing(preferred_path)
+    handoff_path = preferred_path
+
+    if is_new:
+        scaffold_template(handoff_path, args.name)
+    elif args.new_copy_if_exists:
+        handoff_path = generate_unique_path(preferred_path)
+        is_new = True
+        existing_content = None
+        scaffold_template(handoff_path, args.name)
+
+    git_context = gather_git_context(root, default_branch, args.log_count)
+
+    if args.emit_json:
+        output = {
+            "handoff_file": str(handoff_path),
+            "is_new": is_new,
+            "repo_name": repo_name,
+            "name": args.name,
+            "branch_name": branch,
+            "sanitized_name": sanitized,
+            "default_branch": default_branch,
+            "existing_content": existing_content,
+            "git_context": git_context,
+        }
+        print(json.dumps(output, indent=2))
+    else:
+        render_text(
+            handoff_path=handoff_path,
+            is_new=is_new,
+            repo_name=repo_name,
+            branch=branch,
+            default_branch=default_branch,
+            git_context=git_context,
+            existing_content=existing_content,
+        )
+
+    return 0
+
+
+def render_text(
+    *,
+    handoff_path: Path,
+    is_new: bool,
+    repo_name: str,
+    branch: str,
+    default_branch: str,
+    git_context: dict[str, Any],
+    existing_content: str | None,
+) -> None:
+    status = "created" if is_new else "exists"
+    print(f"Handoff file ({status}): {handoff_path}")
+    print(f"Repo: {repo_name}")
+    print(f"Branch: {branch}")
+    print(f"Default branch: {default_branch}")
+
+    ahead = git_context.get("ahead", 0)
+    behind = git_context.get("behind", 0)
+    print(f"Ahead: {ahead}, Behind: {behind}")
+
+    commits = git_context.get("recent_commits", [])
+    if commits:
+        print(f"\nRecent commits ({len(commits)}):")
+        for c in commits:
+            print(f"  {c['hash']} {c['message']}")
+    else:
+        print("\nNo commits ahead of default branch.")
+
+    files = git_context.get("files_changed", [])
+    if files:
+        print(f"\nFiles changed ({len(files)}):")
+        for f in files:
+            print(f"  {f['status']}\t{f['path']}")
+
+    wt = git_context.get("working_tree", {})
+    if any(wt.get(k, 0) > 0 for k in ("modified", "untracked", "staged")):
+        print(
+            f"\nWorking tree: {wt.get('modified', 0)} modified, "
+            f"{wt.get('untracked', 0)} untracked, {wt.get('staged', 0)} staged"
+        )
+
+    if existing_content is not None:
+        print("\n--- Existing handoff content ---")
+        print(existing_content)
+        print("--- End existing content ---")
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
dots/config/claude/skills/Handoff/SKILL.md
@@ -0,0 +1,64 @@
+---
+name: Handoff
+description: Write or update a handoff document so another agent or engineer can continue the work with fresh context. USE WHEN user says 'handoff', 'write handoff', 'create handoff', 'pass this off', or wants to capture context for cross-agent or cross-session continuity.
+---
+
+# Handoff
+
+Capture the current goal, progress, context, and next steps in a concise handoff document. Works for cross-agent handoffs (pi → claude, pi → cursor), cross-session continuity, and human-to-human knowledge transfer.
+
+## Quick Start
+
+```bash
+python3 "<path-to-skill>/scripts/create_handoff.py" --repo "." --name "<descriptive-slug>"
+# Add --json for structured output
+```
+
+## Workflow
+
+1. Inspect current git context and choose a short kebab-case slug describing the work
+2. Run the helper script to scaffold the handoff file with git context
+3. If an existing handoff exists for this slug, read it before updating
+4. Write or update the handoff using script output and current session knowledge
+5. Tell the user where the handoff file lives
+
+Use `--new-copy-if-exists` to create a fresh copy even when the slug already exists.
+
+## Handoff Contents
+
+Include these sections:
+
+### Goal
+One-sentence description of what the work aims to achieve.
+
+### Current Progress
+What has been completed, with specific file paths and commit references.
+
+### What Worked
+Approaches, tools, or patterns that proved successful.
+
+### What Didn't Work
+Dead ends, failed approaches, and why they didn't work. This saves the next agent from repeating mistakes.
+
+### Next Steps
+Concrete, ordered list of what to do next. Be specific — file paths, commands, expected outcomes.
+
+### Notes for Next Agent
+Context that doesn't fit elsewhere: environment quirks, gotchas, related issues, relevant documentation.
+
+## Storage
+
+Handoffs are stored in `~/.local/share/ai/handoffs/<repo-name>/<slug>.md`.
+
+The script auto-detects the repository name from git remote and creates the directory structure.
+
+## Integration
+
+- Works alongside `save_session_to_history` — handoff focuses on continuing work, session summary focuses on documenting what was done
+- The handoff document can be included in a new session's context to resume work
+- Compatible with any AI coding tool that can read markdown files
+
+## Related Skills
+
+- **CORE**: Session saves via `save_session_to_history` (documenting past work)
+- **WritingPlans**: For creating implementation plans (forward-looking)
dots/pi/agent/extensions/handoff.ts
@@ -1,231 +0,0 @@
-/**
- * Handoff extension - transfer context to a new focused session
- *
- * Instead of compacting (which is lossy), handoff extracts what matters
- * for your next task and creates a new session with a generated prompt.
- *
- * Usage:
- *   /handoff now implement this for teams as well
- *   /handoff execute phase one of the plan
- *   /handoff check other places that need this fix
- *
- * The generated prompt appears as a draft in the editor for review/editing.
- */
-
-import { complete, type Message } from "@mariozechner/pi-ai";
-import type { ExtensionAPI, SessionEntry } from "@mariozechner/pi-coding-agent";
-import { BorderedLoader, convertToLlm, serializeConversation } from "@mariozechner/pi-coding-agent";
-import { promises as fs } from "node:fs";
-import * as path from "node:path";
-
-const SYSTEM_PROMPT = `You are a context transfer assistant. Given a conversation history and the user's goal for a new thread, generate a focused prompt that:
-
-1. Summarizes relevant context from the conversation (decisions made, approaches taken, key findings)
-2. Lists any relevant files that were discussed or modified
-3. Clearly states the next task based on the user's goal
-4. Is self-contained - the new thread should be able to proceed without the old conversation
-
-Format your response as a prompt the user can send to start the new thread. Be concise but include all necessary context. Do not include any preamble like "Here's the prompt" - just output the prompt itself.
-
-Example output format:
-## Context
-We've been working on X. Key decisions:
-- Decision 1
-- Decision 2
-
-Files involved:
-- path/to/file1.ts
-- path/to/file2.ts
-
-## Task
-[Clear description of what to do next based on user's goal]`;
-
-// =============================================================================
-// Helper Functions
-// =============================================================================
-
-function getYearMonth(): string {
-	const now = new Date();
-	const year = now.getFullYear();
-	const month = String(now.getMonth() + 1).padStart(2, "0");
-	return `${year}-${month}`;
-}
-
-function formatTimestamp(date: Date): string {
-	return date.toISOString().replace("T", " ").slice(0, 19);
-}
-
-async function saveHandoffToStorage(data: {
-	goal: string;
-	originalSession: string;
-	generatedPrompt: string;
-	model: string;
-	entryCount: number;
-}): Promise<string> {
-	const home = process.env.HOME || process.env.USERPROFILE || "";
-	const yearMonth = getYearMonth();
-	const reviewsDir = path.join(home, ".local", "share", "ai", "sessions", yearMonth);
-	
-	// Ensure directory exists
-	await fs.mkdir(reviewsDir, { recursive: true });
-	
-	const timestamp = Date.now();
-	const filename = `handoff-${timestamp}.md`;
-	const filepath = path.join(reviewsDir, filename);
-	
-	const goalSummary = data.goal.slice(0, 60);
-	const now = new Date();
-	
-	const markdown = `# Handoff: ${goalSummary}
-
-**Created:** ${formatTimestamp(now)}
-**Original Session:** ${data.originalSession}
-**Goal:** ${data.goal}
-
-## Generated Prompt
-
-${data.generatedPrompt}
-
-## Metadata
-
-- Model: ${data.model}
-- Original session entries: ${data.entryCount}
-- Timestamp: ${timestamp}
-
-## Follow-up Actions
-
-- [ ] Create new session with this prompt
-- [ ] Review and refine context
-- [ ] Link related TODOs
-`;
-
-	await fs.writeFile(filepath, markdown, "utf-8");
-	return filepath;
-}
-
-export default function (pi: ExtensionAPI) {
-	pi.registerCommand("handoff", {
-		description: "Transfer context to a new focused session",
-		handler: async (args, ctx) => {
-			if (!ctx.hasUI) {
-				ctx.ui.notify("handoff requires interactive mode", "error");
-				return;
-			}
-
-			if (!ctx.model) {
-				ctx.ui.notify("No model selected", "error");
-				return;
-			}
-
-			const goal = args.trim();
-			if (!goal) {
-				ctx.ui.notify("Usage: /handoff <goal for new thread>", "error");
-				return;
-			}
-
-			// Gather conversation context from current branch
-			const branch = ctx.sessionManager.getBranch();
-			const messages = branch
-				.filter((entry): entry is SessionEntry & { type: "message" } => entry.type === "message")
-				.map((entry) => entry.message);
-
-			if (messages.length === 0) {
-				ctx.ui.notify("No conversation to hand off", "error");
-				return;
-			}
-
-			// Convert to LLM format and serialize
-			const llmMessages = convertToLlm(messages);
-			const conversationText = serializeConversation(llmMessages);
-			const currentSessionFile = ctx.sessionManager.getSessionFile();
-
-			// Generate the handoff prompt with loader UI
-			const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
-				const loader = new BorderedLoader(tui, theme, `Generating handoff prompt...`);
-				loader.onAbort = () => done(null);
-
-				const doGenerate = async () => {
-					const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model!);
-					if (!auth.ok) throw new Error(auth.error);
-
-					const userMessage: Message = {
-						role: "user",
-						content: [
-							{
-								type: "text",
-								text: `## Conversation History\n\n${conversationText}\n\n## User's Goal for New Thread\n\n${goal}`,
-							},
-						],
-						timestamp: Date.now(),
-					};
-
-					const response = await complete(
-						ctx.model!,
-						{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
-						{ apiKey: auth.apiKey, headers: auth.headers, signal: loader.signal },
-					);
-
-					if (response.stopReason === "aborted") {
-						return null;
-					}
-
-					return response.content
-						.filter((c): c is { type: "text"; text: string } => c.type === "text")
-						.map((c) => c.text)
-						.join("\n");
-				};
-
-				doGenerate()
-					.then(done)
-					.catch((err) => {
-						console.error("Handoff generation failed:", err);
-						done(null);
-					});
-
-				return loader;
-			});
-
-			if (result === null) {
-				ctx.ui.notify("Cancelled", "info");
-				return;
-			}
-
-			// Save handoff to ai-storage
-			try {
-				const handoffPath = await saveHandoffToStorage({
-					goal,
-					originalSession: currentSessionFile,
-					generatedPrompt: result,
-					model: ctx.model.id,
-					entryCount: messages.length,
-				});
-				
-				ctx.ui.notify(`Handoff saved: ${path.basename(handoffPath)}`, "success");
-			} catch (error: any) {
-				ctx.ui.notify(`Warning: Failed to save handoff: ${error.message}`, "warning");
-			}
-
-			// Let user edit the generated prompt
-			const editedPrompt = await ctx.ui.editor("Edit handoff prompt", result);
-
-			if (editedPrompt === undefined) {
-				ctx.ui.notify("Cancelled", "info");
-				return;
-			}
-
-			// Create new session with parent tracking
-			const newSessionResult = await ctx.newSession({
-				parentSession: currentSessionFile,
-			});
-
-			if (newSessionResult.cancelled) {
-				ctx.ui.notify("New session cancelled", "info");
-				return;
-			}
-
-			// Set the edited prompt in the main editor for submission
-			ctx.ui.setEditorText(editedPrompt);
-			ctx.ui.notify("Handoff ready. Submit when ready.", "info");
-		},
-	});
-}