Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:36:20 +08:00
commit d7dbabd36a
6 changed files with 348 additions and 0 deletions

View File

@@ -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"
]
}

3
README.md Normal file
View File

@@ -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

148
hooks/entrypoints/pre-compact.sh Executable file
View File

@@ -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:<instructions>`
# 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": "<generated handoff markdown>",
# "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:
<user_instructions>
custom_instructions: $user_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:
<format>
## 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]
</format>"
# 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

View File

@@ -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:<goal>`
# 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

29
hooks/hooks.json Normal file
View File

@@ -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
}
]
}
]
}
}

57
plugin.lock.json Normal file
View File

@@ -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": []
}
}