From d7dbabd36ad5801301dae3371d03c2dc539e0ee6 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sun, 30 Nov 2025 08:36:20 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 12 +++ README.md | 3 + hooks/entrypoints/pre-compact.sh | 148 +++++++++++++++++++++++++++++ hooks/entrypoints/session-start.sh | 99 +++++++++++++++++++ hooks/hooks.json | 29 ++++++ plugin.lock.json | 57 +++++++++++ 6 files changed, 348 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100755 hooks/entrypoints/pre-compact.sh create mode 100755 hooks/entrypoints/session-start.sh create mode 100644 hooks/hooks.json create mode 100644 plugin.lock.json diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..ccd5dab --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "claude-handoff", + "description": "Replace /compact with intelligent context transfer - analyzes current thread and generates focused prompts for new threads based on your goal", + "version": "1.0.0", + "author": { + "name": "Kyle Snow Schwartz", + "email": "kyle.snowschwartz@gmail.com" + }, + "hooks": [ + "./hooks" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d11f898 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# claude-handoff + +Replace /compact with intelligent context transfer - analyzes current thread and generates focused prompts for new threads based on your goal diff --git a/hooks/entrypoints/pre-compact.sh b/hooks/entrypoints/pre-compact.sh new file mode 100755 index 0000000..fb5be8c --- /dev/null +++ b/hooks/entrypoints/pre-compact.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# pre-compact.sh - PreCompact hook for claude-handoff plugin +# +# PURPOSE: +# Generates goal-focused handoff content when user runs `/compact handoff:` +# by forking current session and extracting relevant context immediately. +# +# HOOK EVENT: PreCompact +# - Fires BEFORE compact operations (manual /compact) +# - Only activates when custom_instructions match "handoff:..." format +# - Receives: session_id, transcript_path, trigger, custom_instructions +# +# ARCHITECTURE: +# Uses `claude --resume $session_id --fork-session` to create a snapshot +# of current session, then generates handoff context from the fork before +# the original session gets compacted. +# +# TESTING: +# 1. Enable debug logging in hooks/lib/logging.sh (set LOGGING_ENABLED=true) +# 2. Start a conversation, add substantial context (multiple tool uses) +# 3. Run: /compact handoff:now implement feature X +# 4. Check logs: +# tail -f /tmp/handoff-precompact.log +# 5. Verify state file created with handoff_content: +# cat .git/handoff-pending/handoff-context.json +# 6. Expected state file structure: +# { +# "handoff_content": "", +# "goal": "now implement feature X", +# "trigger": "manual", +# "type": "compact" +# } +# +# NEGATIVE TEST (should not trigger): +# /compact # No state file created +# /compact some other instructions # No state file created +# +# EXIT BEHAVIOR: +# - Always exits 0 (never blocks compact) +# - Returns JSON: {"continue": true, "suppressOutput": true} +# +set -euo pipefail + +# Load logging module +source "${BASH_SOURCE%/*}/../lib/logging.sh" +init_logging "precompact" + +# Fail-open: always succeed, never block compact +trap 'jq -n "{continue:true,suppressOutput:true}" && exit 0' ERR + +# Parse hook input from stdin +input=$(cat) +session_id=$(echo "$input" | jq -r '.session_id') +trigger=$(echo "$input" | jq -r '.trigger // "auto"') +cwd=$(echo "$input" | jq -r '.cwd // "."') +manual_instructions=$(echo "$input" | jq -r '.custom_instructions // ""') + +log "Received input: session_id=$session_id trigger=$trigger cwd=$cwd manual_instructions=$manual_instructions" + +# Only proceed if manual instructions match "handoff:..." format +if [[ ! "$manual_instructions" =~ ^handoff: ]]; then + log "Manual instructions don't match 'handoff:' pattern, skipping handoff" + jq -n '{continue: true, suppressOutput: true}' + exit 0 +fi + +# Extract user instructions after "handoff:" prefix +user_instructions="${manual_instructions#handoff:}" +# Trim leading whitespace +user_instructions="${user_instructions#"${user_instructions%%[![:space:]]*}"}" + +log "Handoff triggered with user instructions: $user_instructions" + +# Change to project directory +cd "$cwd" || exit 0 + +# Create state directory +mkdir -p .git/handoff-pending + +log "Forking session $session_id to generate handoff context..." + +# Build handoff extraction prompt +handoff_prompt="Context window compaction imminent. You are analyzing the current to generate a focused Handoff for the next session. + +Create a focused Handoff message for the next agent to immediately pick up where we left off. This Handoff will be used by the next agent to continue the most recent work related to the user's goal: + + + custom_instructions: $user_instructions + + +Information to potentially include: + +1. **Context from previous session** - What we were working on that's relevant to the new goal +2. **Key decisions/patterns** - Approaches, conventions, or constraints already established +3. **Relevant files** - Paths to files that matter for the new goal (paths only) +4. **Current state** - Where things were left that affects the new work +5. **Blockers/dependencies** - Any issues or prerequisites the new session should know about + +Return a concise markdown summary (max 500 words) structured as: + + + ## Immediate Handoff + [Restate the immediate next steps] + + ## Relevant Context + [Bullet points of relevant technical context] + + ## Key Details + [Specific implementation details, file paths, function names, shell commands] + + ## Important Notes + [Warnings, blockers, or critical information] +" + +# Fork the current session and generate goal-focused handoff content +# --fork-session creates a snapshot without affecting original session +# --model haiku for speed and cost +# --print for headless execution +handoff_exit_code=0 +handoff_content=$(claude --resume "$session_id" --fork-session --model haiku --print "$handoff_prompt") || handoff_exit_code=$? + +# Check if handoff generation succeeded +if [[ $handoff_exit_code -ne 0 ]] || [[ -z "$handoff_content" ]]; then + log "ERROR: Failed to generate handoff content (exit code: $handoff_exit_code)" + jq -n '{continue: true, suppressOutput: true}' + exit 0 +fi + +log "Successfully generated handoff content (${#handoff_content} chars)" + +# Save generated handoff content for SessionStart to inject +jq -n \ + --arg content "$handoff_content" \ + --arg goal "$user_instructions" \ + --arg trigger "$trigger" \ + '{ + handoff_content: $content, + goal: $goal, + trigger: $trigger, + type: "compact" + }' \ + >.git/handoff-pending/handoff-context.json + +log "Handoff content saved to .git/handoff-pending/handoff-context.json" + +# Success - allow compact to proceed, suppress output so user doesn't see handoff generation +jq -n '{continue: true, suppressOutput: true}' +exit 0 diff --git a/hooks/entrypoints/session-start.sh b/hooks/entrypoints/session-start.sh new file mode 100755 index 0000000..5e45eb9 --- /dev/null +++ b/hooks/entrypoints/session-start.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# session-start.sh - SessionStart hook for claude-handoff plugin +# +# PURPOSE: +# Injects pre-generated handoff content after `/compact handoff:` +# completes. Content was generated by PreCompact hook using --fork-session. +# +# HOOK EVENT: SessionStart (matcher: "compact") +# - Fires when sessions continue after compact operations (NOT new sessions!) +# - Receives: session_id, transcript_path, cwd, source ("compact") +# +# BEHAVIOR: +# - Checks for handoff content saved by pre-compact.sh +# - Injects handoff_content as systemMessage +# - Cleans up state file after successful injection +# +# TESTING: +# 1. Enable debug logging in hooks/lib/logging.sh (set LOGGING_ENABLED=true) +# 2. Run: /compact handoff:implement feature X +# 3. Compact completes, session continues +# 4. Check logs: +# tail -f /tmp/handoff-sessionstart.log +# 5. Verify handoff appears as system message in continuing session +# 6. Check state file cleaned up on success: +# ls -la .git/handoff-pending/ # should not exist after successful handoff +# +# MANUAL TESTING WITH FAKE STATE: +# # Create fake state with pre-generated content +# mkdir -p .git/handoff-pending +# echo '{"handoff_content":"## Goal\nTest\n## Context\n- Item 1","goal":"test","trigger":"manual","type":"compact"}' > .git/handoff-pending/handoff-context.json +# +# # Run hook +# echo '{"session_id":"test-123","cwd":"'$(pwd)'","source":"compact"}' | bash session-start.sh +# +# # Check output contains systemMessage with handoff content +# # Cleanup +# rm -rf .git/handoff-pending +# +# EXIT BEHAVIOR: +# - Returns JSON with systemMessage if handoff content exists +# - Exits silently (exit 0, no output) if no handoff state found +# +set -euo pipefail + +# Load logging module +source "${BASH_SOURCE%/*}/../lib/logging.sh" +init_logging "sessionstart" + +# Read hook input +input=$(cat) +session_id=$(echo "$input" | jq -r '.session_id // ""') +cwd=$(echo "$input" | jq -r '.cwd // "."') +source=$(echo "$input" | jq -r '.source // "unknown"') + +log "Received input: session_id=$session_id cwd=$cwd source=$source" + +# Only proceed if this is a compact-triggered session continuation +if [[ "$source" != "compact" ]]; then + log "Source is '$source', not 'compact'. Exiting." + exit 0 +fi + +# Change to project directory +cd "$cwd" || exit 0 + +# Check for pending handoff state +state_file=".git/handoff-pending/handoff-context.json" + +if [[ ! -f "$state_file" ]]; then + log "No state file found, exiting" + exit 0 +fi + +log "Found state file: $state_file" + +# Read pre-generated handoff content +handoff_content=$(cat "$state_file" | jq -r '.handoff_content // ""') +goal=$(cat "$state_file" | jq -r '.goal // ""') + +log "State: goal='$goal' content_length=${#handoff_content} chars" + +# If no handoff content, exit without cleanup (might be old state format) +if [[ -z "$handoff_content" ]]; then + log "No handoff_content in state file, exiting" + exit 0 +fi + +# Cleanup: remove both file and directory +rm -f "$state_file" +rmdir .git/handoff-pending 2>/dev/null || true + +log "Injecting handoff content as systemMessage" + +# Return JSON with systemMessage to inject into session +jq -n --arg context "$handoff_content" '{ + systemMessage: $context +}' + +exit 0 diff --git a/hooks/hooks.json b/hooks/hooks.json new file mode 100644 index 0000000..4311dd0 --- /dev/null +++ b/hooks/hooks.json @@ -0,0 +1,29 @@ +{ + "description": "Claude-Handoff: Intelligent context transfer via /compact detection", + "hooks": { + "PreCompact": [ + { + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/pre-compact.sh", + "description": "Save session state before compact for handoff generation" + } + ] + } + ], + "SessionStart": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/hooks/entrypoints/session-start.sh", + "description": "Generate and inject handoff context after /compact", + "timeout": 60 + } + ] + } + ] + } +} diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..e279e00 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,57 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:kylesnowschwartz/claude-handoff:handoff-plugin", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "af25954ad2d342f3404c5f46437cc42a40ff6a94", + "treeHash": "696f679828acff0e0906a1d600ebb262520d70911113e3c264961d04efb60193", + "generatedAt": "2025-11-28T10:20:02.217245Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "claude-handoff", + "description": "Replace /compact with intelligent context transfer - analyzes current thread and generates focused prompts for new threads based on your goal", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "c51395e86a2c2546d83c40d56240066005a3b0ce7382a3906e58ce2a91c23852" + }, + { + "path": "hooks/hooks.json", + "sha256": "751e016a80c1006a67f35def0164433241e100f5744e513bbe8180106afaed9d" + }, + { + "path": "hooks/lib/logging.sh", + "sha256": "31aa174911a5a582c72c5508624b3d95f1d8adb4b9fa0cdbb1bb59c24a238f45" + }, + { + "path": "hooks/entrypoints/pre-compact.sh", + "sha256": "4ad163a780152021db0aac736600586ccc0b89cd19b0f688cc0cc2f92eb402d7" + }, + { + "path": "hooks/entrypoints/session-start.sh", + "sha256": "073d7e1b9a6cc2fd98ab20bcf4b7227831560545242a9a63d5f7b91ba62690b4" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "5138081115cf457df74067faab916b08fdf1ca67322348ee0a18dbea758a84ca" + } + ], + "dirSha256": "696f679828acff0e0906a1d600ebb262520d70911113e3c264961d04efb60193" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file