Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:46:45 +08:00
commit eb32390526
15 changed files with 1350 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
{
"name": "worktree-task",
"description": "Manage large coding tasks using git worktrees and background Claude Code sessions. Supports launching, monitoring, resuming, and cleanup of autonomous tasks with alert notifications.",
"version": "1.0.0",
"author": {
"name": "ourines"
},
"skills": [
"./skills"
],
"commands": [
"./commands"
],
"hooks": [
"./hooks"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# worktree-task
Manage large coding tasks using git worktrees and background Claude Code sessions. Supports launching, monitoring, resuming, and cleanup of autonomous tasks with alert notifications.

47
commands/cleanup.md Normal file
View File

@@ -0,0 +1,47 @@
---
description: Clean up a completed worktree task
allowed-tools: Bash
---
# Cleanup Worktree Task
Clean up a completed or abandoned worktree task by killing the tmux session and optionally removing the git worktree.
## Usage
Provide:
1. **Session name** - The tmux session to clean up
2. **--remove-worktree** (optional) - Also remove the git worktree directory
## Example
User: "Cleanup the my-feature worktree task"
User: "Cleanup my-feature and remove the worktree"
## Execution
```bash
# Kill session only (keep worktree for review)
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup.py <session-name>
# Kill session AND remove worktree
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup.py <session-name> --remove-worktree
```
## What It Does
1. Kills the tmux session
2. Lists remaining git worktrees
3. If `--remove-worktree`: removes the worktree directory
4. Shows next steps (merge branch, create PR)
## After Cleanup
The branch still exists. To complete:
- Merge: `git merge <branch-name>`
- Create PR: `gh pr create --head <branch-name>`
- Delete branch: `git branch -d <branch-name>`
## Note
Always review changes before removing the worktree. Use `/worktree:status` to check commits first.

69
commands/help.md Normal file
View File

@@ -0,0 +1,69 @@
---
description: Show help for worktree-task plugin
allowed-tools: ""
---
# Worktree Task Plugin Help
A plugin for managing large coding tasks using git worktrees and background Claude Code sessions.
## Commands
| Command | Description |
|---------|-------------|
| `/worktree-task:launch <task>` | Launch a new background task in a separate worktree |
| `/worktree-task:status [name]` | Check status of all or specific task |
| `/worktree-task:resume <name>` | Resume an interrupted task |
| `/worktree-task:cleanup <name>` | Clean up completed task and worktree |
| `/worktree-task:merge <branch>` | Merge a feature branch into current branch |
| `/worktree-task:rebase <branch>` | Rebase current branch onto a feature branch |
| `/worktree-task:help` | Show this help message |
## Quick Start
```bash
# 1. Launch a complex task in the background
/worktree-task:launch "Implement user authentication with OAuth2"
# 2. Check progress
/worktree-task:status
# 3. When done, merge the changes
/worktree-task:merge feature-auth
# 4. Clean up
/worktree-task:cleanup feature-auth
```
## How It Works
1. **Launch** creates a git worktree (isolated copy of your repo) and spawns a Claude Code instance in a tmux session
2. **Monitor** via `/worktree-task:status` to see progress, logs, and completion state
3. **Resume** if task gets interrupted (rate limits, API errors, network issues)
4. **Merge/Rebase** with automatic conflict resolution powered by Claude
5. **Cleanup** removes the tmux session and worktree when done
## Best Practices
- Use for tasks that take 10+ minutes or require extensive changes
- Keep task descriptions clear and specific
- Check status periodically to monitor progress
- Use `--keep-worktree` with cleanup if you want to review changes first
## File Locations
- Worktrees: Created in parent directory of your repo (e.g., `../worktree-task-<name>`)
- Task prompts: `/tmp/claude_task_prompt.txt` (temporary)
- Monitor logs: `<plugin-dir>/.monitor_cron.log`
## Requirements
- macOS (for notifications)
- tmux
- git
- Python 3.8+
## Links
- GitHub: https://github.com/ourines/worktree-task-plugin
- Issues: https://github.com/ourines/worktree-task-plugin/issues

42
commands/launch.md Normal file
View File

@@ -0,0 +1,42 @@
---
description: Launch a new background worktree task
allowed-tools: Bash
---
# Launch Worktree Task
Launch a background Claude Code session in a separate git worktree to execute a large task autonomously.
## Usage
Provide:
1. **Branch name** - The git branch for this task (e.g., `feature/my-task`)
2. **Task description** - What the background Claude should do
## Example
User: "Launch a worktree task on branch feature/auth to implement the authentication module"
## Execution
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/launch.py "<branch-name>" "<task-description>"
```
## Prerequisites
- Git working directory must be clean (commit or stash changes first)
- tmux must be installed
- Branch name will be created if it doesn't exist
## What Happens
1. Creates a git worktree at `../<project>-<branch-name>`
2. Creates a tmux session named after the branch
3. Launches Claude Code with `--dangerously-skip-permissions`
4. Sends the task prompt with instructions to use Task tool for each phase
## After Launch
Use `/worktree:status` to monitor progress or `tmux attach -t <session>` to take over interactively.

44
commands/merge.md Normal file
View File

@@ -0,0 +1,44 @@
---
description: Merge a feature branch into current branch
allowed-tools: Bash
---
# Merge Feature Branch
Merge a completed feature branch into the current branch using worktree + tmux with automatic conflict resolution.
## Usage
Provide the feature branch name to merge into the current branch.
## Example
User on `dev` branch: "Merge feature-auth into dev"
User on `main` branch: "Merge feature/new-api into main"
## Execution
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/merge.py "<feature-branch>"
```
## Prerequisites
- Must be on the target branch (e.g., dev, main)
- Feature branch must exist
- tmux must be installed
## What Happens
1. Checks if feature branch has a worktree
2. If worktree exists:
- Enters the worktree
- Rebases feature branch onto current branch
- Resolves any conflicts
3. Returns to main repo
4. Merges feature branch into current branch
5. Resolves any merge conflicts using Claude Code in tmux
## After Launch
Use `tmux attach -t merge-<feature>-to-<target>` to monitor progress.

44
commands/rebase.md Normal file
View File

@@ -0,0 +1,44 @@
---
description: Rebase current branch onto a feature branch
allowed-tools: Bash
---
# Rebase onto Feature Branch
Rebase the current branch onto a feature branch using worktree + tmux with automatic conflict resolution.
## Usage
Provide the feature branch name to rebase the current branch onto.
## Example
User on `dev` branch: "Rebase dev onto feature-auth"
User on `main` branch: "Rebase main onto feature/new-api"
## Execution
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/rebase.py "<feature-branch>"
```
## Prerequisites
- Must be on the target branch (e.g., dev, main)
- Feature branch must exist
- tmux must be installed
## What Happens
1. Checks if feature branch has a worktree
2. If worktree exists:
- Enters the worktree
- Rebases feature branch onto current branch
- Resolves any conflicts
3. Returns to main repo
4. Rebases current branch onto feature branch
5. Resolves any rebase conflicts using Claude Code in tmux
## After Launch
Use `tmux attach -t rebase-<target>-onto-<feature>` to monitor progress.

51
commands/resume.md Normal file
View File

@@ -0,0 +1,51 @@
---
description: Resume an interrupted worktree task
allowed-tools: Bash
---
# Resume Worktree Task
Resume a background worktree task that was interrupted (rate limit, API error, timeout, or waiting for input).
## Usage
Provide:
1. **Session name** - The tmux session to resume
2. **Message** (optional) - Custom instruction for Claude
## Example
User: "Resume the my-feature worktree task"
User: "Resume my-feature with message 'Continue from phase 4'"
## Execution
```bash
# Auto-detect error and send appropriate message
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/resume.py <session-name>
# Send custom message
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/resume.py <session-name> "<message>"
# Retry last failed operation
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/resume.py <session-name> --retry
# Check status only (don't send message)
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/resume.py <session-name> --check
```
## Auto-Detection
The script automatically detects:
- `rate_limit` - Waits and sends resume message
- `api_error` - Sends retry message
- `timeout` - Sends retry message
- `waiting_input` - Sends continue message
## What It Does
1. Checks if session exists
2. Captures recent output to detect error type
3. Generates appropriate resume message
4. Sends message to Claude with Enter confirmation
5. Shows response after brief wait

43
commands/status.md Normal file
View File

@@ -0,0 +1,43 @@
---
description: Check status of worktree tasks
allowed-tools: Bash
---
# Worktree Task Status
Check the status of background worktree tasks.
## Usage
- Without arguments: List all active tmux sessions and git worktrees
- With session name: Show detailed status for that session
## Example
User: "Check status of worktree tasks"
User: "What's the status of the my-feature task?"
## Execution
```bash
# List all tasks
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/status.py
# Check specific task
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/status.py <session-name>
```
## Output Includes
- Active tmux sessions
- Git worktrees
- Current branch and changed files
- Recent commits
- Last activity (tmux output)
## Quick Actions
After checking status, you can:
- `/worktree:resume <session>` - Resume an interrupted task
- `/worktree:cleanup <session>` - Clean up a completed task
- `tmux attach -t <session>` - Take over interactively

143
hooks/handlers/on-session-end.py Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
Hook handler for SessionEnd event.
Sends macOS notification and logs when a worktree session ends.
"""
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
def send_macos_notification(title: str, message: str, sound: str = "default"):
"""Send a macOS notification using osascript."""
script = f'''
display notification "{message}" with title "{title}" sound name "{sound}"
'''
subprocess.run(["osascript", "-e", script], capture_output=True)
def send_terminal_notifier(title: str, message: str, subtitle: str = None):
"""Send notification via terminal-notifier if available."""
try:
cmd = [
"terminal-notifier",
"-title", title,
"-message", message,
"-sound", "default",
"-group", "worktree-task"
]
if subtitle:
cmd.extend(["-subtitle", subtitle])
subprocess.run(cmd, capture_output=True, check=True)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def log_session_end(session_id: str, reason: str, cwd: str):
"""Log session end to a file for history tracking."""
log_dir = Path.home() / ".claude" / "plugins" / "worktree-task" / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log_file = log_dir / "session-history.log"
entry = {
"timestamp": datetime.now().isoformat(),
"session_id": session_id,
"reason": reason,
"cwd": cwd
}
with open(log_file, "a") as f:
f.write(json.dumps(entry) + "\n")
def get_session_info() -> dict:
"""Try to determine session context."""
cwd = os.getcwd()
info = {
"cwd": cwd,
"is_worktree": False,
"branch": None,
"project": None
}
# Get current branch
try:
branch_result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True, text=True, cwd=cwd
)
if branch_result.returncode == 0:
info["branch"] = branch_result.stdout.strip()
except Exception:
pass
# Check if worktree by path pattern
if "-" in os.path.basename(cwd) and "worktree" not in cwd.lower():
# Might be a worktree like "project-feature-branch"
info["is_worktree"] = True
# Also check git worktree list
try:
result = subprocess.run(
["git", "rev-parse", "--git-common-dir"],
capture_output=True, text=True, cwd=cwd
)
git_common = result.stdout.strip()
git_dir_result = subprocess.run(
["git", "rev-parse", "--git-dir"],
capture_output=True, text=True, cwd=cwd
)
git_dir = git_dir_result.stdout.strip()
# If git-dir != git-common-dir, we're in a worktree
if git_common != git_dir and ".git/worktrees" in git_dir:
info["is_worktree"] = True
except Exception:
pass
# Extract project name
info["project"] = os.path.basename(cwd)
return info
def main():
# Read hook input from stdin
try:
hook_input = json.load(sys.stdin)
except json.JSONDecodeError:
hook_input = {}
session_id = hook_input.get("session_id", "unknown")
reason = hook_input.get("reason", "unknown") # e.g., "clear", "logout", "exit"
session_info = get_session_info()
# Log all worktree session ends
if session_info.get("is_worktree"):
log_session_end(session_id, reason, session_info.get("cwd", ""))
branch = session_info.get("branch", "unknown")
project = session_info.get("project", "unknown")
title = "Worktree Session Ended"
message = f"Branch: {branch}"
subtitle = f"Reason: {reason}"
# Try terminal-notifier first, fallback to osascript
if not send_terminal_notifier(title, message, subtitle):
send_macos_notification(title, f"{message} ({reason})")
# Always exit 0 to not block Claude
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,467 @@
#!/usr/bin/env python3
"""
Hook handler for SessionStart event.
Checks if the plugin has upstream updates available and notifies the user.
This hook runs at session start and provides a non-blocking notification
if updates are available, without interrupting the session.
Update detection uses Claude Code's plugin system:
- Reads installed plugin info from ~/.claude/plugins/installed_plugins.json
- Fetches latest release info from GitHub Releases API
- Suggests using /plugin commands to update
"""
import json
import os
import subprocess
import sys
import time
import urllib.request
import urllib.error
from pathlib import Path
# Cache settings
CACHE_FILE = Path.home() / ".claude" / "plugins" / ".worktree-task-update-cache.json"
CACHE_TTL_SECONDS = 24 * 60 * 60 # 24 hours
# Plugin identification
PLUGIN_NAME = "worktree-task"
MARKETPLACE_NAME = "worktree-task-plugin"
PLUGIN_ID = f"{PLUGIN_NAME}@{MARKETPLACE_NAME}"
# GitHub repository info
GITHUB_OWNER = "ourines"
GITHUB_REPO = "worktree-task-plugin"
GITHUB_RELEASES_API = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/releases/latest"
# Promotional links (shown occasionally in update notifications)
PROMO_LINKS = {
"twitter": "https://x.com/ourines_",
"github": f"https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}",
}
def get_claude_plugins_dir() -> Path:
"""Get the Claude Code plugins directory."""
return Path.home() / ".claude" / "plugins"
def load_cache() -> dict:
"""Load update check cache from disk."""
if not CACHE_FILE.exists():
return {}
try:
with open(CACHE_FILE, "r") as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return {}
def save_cache(cache: dict) -> None:
"""Save update check cache to disk."""
try:
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(CACHE_FILE, "w") as f:
json.dump(cache, f)
except IOError:
pass
def is_cache_valid(cache: dict) -> bool:
"""Check if cache is still valid (within TTL)."""
last_check = cache.get("last_check", 0)
return (time.time() - last_check) < CACHE_TTL_SECONDS
def get_installed_plugin_info() -> dict:
"""
Read installed plugin info from Claude Code's installed_plugins.json.
Returns dict with:
- version: str
- gitCommitSha: str
- installPath: str
- found: bool
"""
result = {
"version": "",
"gitCommitSha": "",
"installPath": "",
"found": False
}
plugins_file = get_claude_plugins_dir() / "installed_plugins.json"
if not plugins_file.exists():
return result
try:
with open(plugins_file, "r") as f:
data = json.load(f)
plugin_info = data.get("plugins", {}).get(PLUGIN_ID, {})
if plugin_info:
result["version"] = plugin_info.get("version", "")
result["gitCommitSha"] = plugin_info.get("gitCommitSha", "")
result["installPath"] = plugin_info.get("installPath", "")
result["found"] = True
except (json.JSONDecodeError, IOError):
pass
return result
def get_marketplace_info() -> dict:
"""
Read marketplace info from Claude Code's known_marketplaces.json.
Returns dict with:
- repo: str (e.g., "ourines/worktree-task-plugin")
- installLocation: str
- found: bool
"""
result = {
"repo": "",
"installLocation": "",
"found": False
}
marketplaces_file = get_claude_plugins_dir() / "known_marketplaces.json"
if not marketplaces_file.exists():
return result
try:
with open(marketplaces_file, "r") as f:
data = json.load(f)
marketplace_info = data.get(MARKETPLACE_NAME, {})
if marketplace_info:
source = marketplace_info.get("source", {})
result["repo"] = source.get("repo", "")
result["installLocation"] = marketplace_info.get("installLocation", "")
result["found"] = True
except (json.JSONDecodeError, IOError):
pass
return result
def run_git_command(cmd: list, cwd: str = None) -> tuple[bool, str]:
"""Run a git command and return (success, output)."""
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=cwd,
timeout=5 # Reduced from 15s to 5s for faster startup
)
return result.returncode == 0, result.stdout.strip()
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
return False, ""
def get_local_commit_sha(install_path: str) -> str:
"""Get the actual commit SHA from the installed plugin directory (not from installed_plugins.json)."""
if not install_path or not os.path.isdir(install_path):
return ""
success, sha = run_git_command(["git", "rev-parse", "HEAD"], install_path)
return sha if success else ""
def fetch_github_release() -> dict:
"""
Fetch latest release info from GitHub Releases API.
Returns dict with:
- tag_name: str (e.g., "v1.0.0")
- name: str (release title)
- body: str (release notes in markdown)
- published_at: str
- html_url: str (link to release page)
- found: bool
- error: str
"""
result = {
"tag_name": "",
"name": "",
"body": "",
"published_at": "",
"html_url": "",
"found": False,
"error": ""
}
try:
req = urllib.request.Request(
GITHUB_RELEASES_API,
headers={
"Accept": "application/vnd.github.v3+json",
"User-Agent": "worktree-task-plugin"
}
)
with urllib.request.urlopen(req, timeout=3) as response:
data = json.loads(response.read().decode("utf-8"))
result["tag_name"] = data.get("tag_name", "")
result["name"] = data.get("name", "")
result["body"] = data.get("body", "")
result["published_at"] = data.get("published_at", "")
result["html_url"] = data.get("html_url", "")
result["found"] = True
except urllib.error.HTTPError as e:
if e.code == 404:
result["error"] = "No releases found"
else:
result["error"] = f"HTTP error: {e.code}"
except urllib.error.URLError as e:
result["error"] = f"Network error: {e.reason}"
except (json.JSONDecodeError, TimeoutError) as e:
result["error"] = str(e)
return result
def parse_version(tag: str) -> tuple:
"""Parse version tag like 'v1.2.3' into tuple (1, 2, 3) for comparison."""
import re
match = re.match(r"v?(\d+)\.(\d+)\.(\d+)", tag)
if match:
return tuple(int(x) for x in match.groups())
return (0, 0, 0)
def check_remote_updates(install_path: str, local_sha: str, local_version: str) -> dict:
"""
Check if there are updates available using GitHub Releases API.
Falls back to git commit comparison if no releases found.
Returns dict with:
- has_updates: bool
- update_type: str ("release" or "commit")
- remote_version: str (for release updates)
- remote_sha: str (for commit updates)
- release_name: str
- release_notes: str
- release_url: str
- behind_count: int
- error: str
"""
result = {
"has_updates": False,
"update_type": "",
"remote_version": "",
"remote_sha": "",
"release_name": "",
"release_notes": "",
"release_url": "",
"behind_count": 0,
"error": ""
}
# Try GitHub Releases API first
release_info = fetch_github_release()
if release_info["found"]:
remote_version = release_info["tag_name"]
result["remote_version"] = remote_version
result["release_name"] = release_info["name"] or remote_version
result["release_url"] = release_info["html_url"]
# Parse release notes (truncate if too long)
body = release_info["body"] or ""
if len(body) > 500:
body = body[:500] + "..."
result["release_notes"] = body
# Compare versions
if local_version:
local_ver = parse_version(local_version)
remote_ver = parse_version(remote_version)
if remote_ver > local_ver:
result["has_updates"] = True
result["update_type"] = "release"
return result
# Fallback: Check git commits if no release or version match
if not install_path or not os.path.isdir(install_path):
result["error"] = release_info.get("error", "Install path not found")
return result
# Check if it's a git repository
success, _ = run_git_command(["git", "rev-parse", "--git-dir"], install_path)
if not success:
result["error"] = "Not a git repository"
return result
# Fetch updates from remote (silent)
success, _ = run_git_command(["git", "fetch", "--quiet", "origin"], install_path)
if not success:
result["error"] = "Failed to fetch from remote"
return result
# Get current branch
success, branch = run_git_command(["git", "branch", "--show-current"], install_path)
if not success:
branch = "main"
# Get remote HEAD commit
remote_ref = f"origin/{branch}"
success, remote_sha = run_git_command(["git", "rev-parse", remote_ref], install_path)
if success:
result["remote_sha"] = remote_sha[:8]
# Compare with local SHA
if local_sha and remote_sha:
if local_sha != remote_sha and not remote_sha.startswith(local_sha[:8]):
success, behind_count = run_git_command(
["git", "rev-list", "--count", f"{local_sha}..{remote_ref}"],
install_path
)
if success and behind_count.isdigit():
count = int(behind_count)
if count > 0:
result["has_updates"] = True
result["update_type"] = "commit"
result["behind_count"] = count
return result
def format_release_notes(notes: str, max_lines: int = 5) -> str:
"""Format release notes for display, limiting lines."""
if not notes:
return ""
lines = notes.strip().split("\n")
formatted = []
for line in lines[:max_lines]:
# Clean up markdown formatting for terminal display
line = line.strip()
if line.startswith("- "):
formatted.append(f"{line[2:]}")
elif line.startswith("* "):
formatted.append(f"{line[2:]}")
elif line.startswith("# "):
continue # Skip headers
elif line.startswith("## "):
formatted.append(f" {line[3:]}")
elif line:
formatted.append(f" {line}")
if len(lines) > max_lines:
formatted.append(f" ... and {len(lines) - max_lines} more")
return "\n".join(formatted)
def main():
# Read hook input from stdin
try:
hook_input = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
hook_input = {}
source = hook_input.get("source", "startup")
# Only check on startup, skip for resume/clear/compact to avoid repeated checks
if source != "startup":
sys.exit(0)
# Prepare output
output = {
"hookSpecificOutput": {
"hookEventName": "SessionStart"
}
}
# Get installed plugin info
plugin_info = get_installed_plugin_info()
if not plugin_info["found"]:
# Plugin not found in installed_plugins.json, skip check
print(json.dumps(output))
sys.exit(0)
# Get marketplace info
marketplace_info = get_marketplace_info()
install_path = plugin_info["installPath"] or marketplace_info["installLocation"]
if not install_path:
print(json.dumps(output))
sys.exit(0)
# Get actual local commit SHA from install directory (not from installed_plugins.json which may be stale)
local_sha = get_local_commit_sha(install_path)
if not local_sha:
# Fallback to installed_plugins.json if git command fails
local_sha = plugin_info["gitCommitSha"]
local_version = plugin_info["version"]
# Check cache - skip network requests if recently checked with same local SHA
cache = load_cache()
if is_cache_valid(cache) and cache.get("local_sha") == local_sha:
# Use cached result
if cache.get("has_updates") and cache.get("message"):
output["systemMessage"] = cache["message"]
print(json.dumps(output))
sys.exit(0)
# Check for updates (use version for release comparison)
update_info = check_remote_updates(install_path, local_sha, local_version)
message = None
if update_info.get("has_updates"):
update_type = update_info["update_type"]
if update_type == "release":
# Release-based update notification
remote_version = update_info["remote_version"]
release_name = update_info["release_name"]
message = f"🚀 Worktree Task Plugin: New release available!"
message += f"\n {local_version or 'current'}{remote_version}"
if release_name and release_name != remote_version:
message += f" ({release_name})"
# Show release notes
if update_info["release_notes"]:
formatted_notes = format_release_notes(update_info["release_notes"])
if formatted_notes:
message += f"\n\n📋 What's New:\n{formatted_notes}"
# Release URL
if update_info["release_url"]:
message += f"\n\n🔗 Details: {update_info['release_url']}"
else:
# Commit-based update notification (fallback)
behind_count = update_info["behind_count"]
local_sha_short = local_sha[:8] if local_sha else "unknown"
remote_sha = update_info["remote_sha"]
message = f"🔄 Worktree Task Plugin: {behind_count} update(s) available"
if local_sha_short and remote_sha:
message += f" ({local_sha_short}{remote_sha})"
# Update commands
message += f"\n\n📦 To update:\n"
message += f" /plugin uninstall {PLUGIN_ID}\n"
message += f" /plugin install {PLUGIN_ID}"
# Promotional footer (subtle, at the very end)
message += f"\n\n─────────────────────────────"
message += f"\n⭐ Like this plugin? Star us: {PROMO_LINKS['github']}"
output["systemMessage"] = message
# Save to cache
save_cache({
"last_check": time.time(),
"local_sha": local_sha,
"has_updates": update_info.get("has_updates", False),
"message": message
})
# Output JSON result
print(json.dumps(output))
sys.exit(0)
if __name__ == "__main__":
main()

152
hooks/handlers/on-stop.py Executable file
View File

@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
Hook handler for Stop event.
Sends macOS notification when a worktree task session stops.
Only triggers for sessions running inside a tmux worktree-task session,
not regular Claude Code sessions.
"""
import json
import os
import random
import subprocess
import sys
# Promotional messages - shown occasionally in notifications
PROMO_MESSAGES = [
"⭐ Star us on GitHub!",
"🚀 Powered by worktree-task-plugin",
"💡 Try codex.markets for more API capacity",
]
# Probability of showing promo (30%)
PROMO_CHANCE = 0.3
def is_worktree_task_session() -> bool:
"""
Check if we're running inside a worktree-task tmux session.
Returns True only if:
1. TMUX env var is set (running inside tmux)
2. Session name starts with 'worktree-'
"""
# Must be in tmux
if not os.environ.get("TMUX"):
return False
# Check tmux session name
try:
result = subprocess.run(
["tmux", "display-message", "-p", "#S"],
capture_output=True, text=True, check=True
)
session_name = result.stdout.strip()
return session_name.startswith("worktree-")
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def send_macos_notification(title: str, message: str, sound: str = "default") -> bool:
"""
Send a macOS notification using osascript.
Returns True if notification was sent successfully, False otherwise.
"""
# Escape quotes in message
escaped_message = message.replace('"', '\\"')
escaped_title = title.replace('"', '\\"')
script = f'display notification "{escaped_message}" with title "{escaped_title}" sound name "{sound}"'
try:
result = subprocess.run(
["osascript", "-e", script],
capture_output=True,
check=True,
timeout=5
)
return True
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
return False
def get_session_info() -> dict:
"""Get info about the current session."""
cwd = os.getcwd()
info = {
"cwd": cwd,
"branch": None,
"tmux_session": None
}
# Get current branch
try:
result = subprocess.run(
["git", "branch", "--show-current"],
capture_output=True, text=True, cwd=cwd
)
if result.returncode == 0:
info["branch"] = result.stdout.strip()
except FileNotFoundError:
pass
# Get tmux session name
if os.environ.get("TMUX"):
try:
result = subprocess.run(
["tmux", "display-message", "-p", "#S"],
capture_output=True, text=True
)
if result.returncode == 0:
info["tmux_session"] = result.stdout.strip()
except FileNotFoundError:
pass
return info
def main():
# Early exit: only run for worktree-task sessions
if not is_worktree_task_session():
sys.exit(0)
# Read hook input from stdin (may be empty)
try:
hook_input = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
hook_input = {}
session_info = get_session_info()
branch = session_info.get("branch") or "unknown"
tmux_session = session_info.get("tmux_session") or "worktree"
# Extract task name from tmux session (worktree-{task_name})
task_name = tmux_session.replace("worktree-", "") if tmux_session else "task"
title = "✅ Worktree Task Completed"
message = f"'{task_name}' on branch '{branch}' finished"
# Occasionally add a subtle promo
promo = ""
if random.random() < PROMO_CHANCE:
promo = random.choice(PROMO_MESSAGES)
# Try macOS notification first
notification_message = f"{message}\n{promo}" if promo else message
notification_sent = send_macos_notification(title, notification_message)
# Fallback: output systemMessage (always visible to user, no context cost)
# Only if notification failed or as a reliable backup
if not notification_sent:
output = {
"systemMessage": f"{title}\n{message}" + (f"\n{promo}" if promo else "")
}
print(json.dumps(output))
# Always exit 0 to not block Claude
sys.exit(0)
if __name__ == "__main__":
main()

37
hooks/hooks.json Normal file
View File

@@ -0,0 +1,37 @@
{
"description": "Worktree task hooks for alerts and update checking",
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/handlers/on-session-start.py",
"timeout": 15
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/handlers/on-stop.py"
}
]
}
],
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/handlers/on-session-end.py"
}
]
}
]
}
}

89
plugin.lock.json Normal file
View File

@@ -0,0 +1,89 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:ourines/worktree-task-plugin:",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "bfbd17c264488863f712a5fe055b2f5daa914f00",
"treeHash": "54232ed2470330f615270dbd71501fa94b2ef899e22d45294cf8d10cb4981af0",
"generatedAt": "2025-11-28T10:27:34.122887Z",
"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": "worktree-task",
"description": "Manage large coding tasks using git worktrees and background Claude Code sessions. Supports launching, monitoring, resuming, and cleanup of autonomous tasks with alert notifications.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d6a8798bffef5a5b8a4a8d2584607c7d8074838a7b8c05b4c5868be46d78786a"
},
{
"path": "hooks/hooks.json",
"sha256": "8df5519f25e5b94bfba50b99e1ecc8d8a4011f94fa62f30e68fbfe65c4f68942"
},
{
"path": "hooks/handlers/on-session-start.py",
"sha256": "bbc3a8aaf9afa46e740681cd9e8ee3ce315acf4e1542bb07bd8240466950089a"
},
{
"path": "hooks/handlers/on-session-end.py",
"sha256": "0344c53b426d17392a628b88cb065af0e8c73bf1846c04a69e971ed63e167c0d"
},
{
"path": "hooks/handlers/on-stop.py",
"sha256": "14ded1dac6fce0e7c5ed69b39e6ff62308c950ad9939e6f09dbf98d19b620a81"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "87c9e3e6754e05f47a67647cd6915fac99babe1c4924648edb33fd5ccb3030b2"
},
{
"path": "commands/launch.md",
"sha256": "8378745e6a545701863840ab3fa975a0fccca6aaa555cf7650f9a22f8a95e06a"
},
{
"path": "commands/status.md",
"sha256": "8d2bb5e27d3f2ecf9655698ba751216ef939ced49faaebe58f454a819465626f"
},
{
"path": "commands/help.md",
"sha256": "dbdb1ec7b71ababf51fd062e51912838496b47641f373768c014106717539021"
},
{
"path": "commands/resume.md",
"sha256": "2ca97ca6386b53836162a130e554ab07058be6345b17bc2ec57d578f96386603"
},
{
"path": "commands/rebase.md",
"sha256": "fe088ed85387511b3a913d7dd1e44c3ab9377d7ac23f3f52fbcf589c34ae7e8a"
},
{
"path": "commands/merge.md",
"sha256": "7289678aa6fa79c4d7ced51371a15dc3e7aa570fee7009047bc48c3b2cc4e4f8"
},
{
"path": "commands/cleanup.md",
"sha256": "4135fe2582cb2f205edbc09dd4eb210e2b1f6636149ac571ba756f8f2c69a6c3"
},
{
"path": "skills/worktree-task/SKILL.md",
"sha256": "f64d9d23d4c23c7c5adfa1284f6484930eccd3abe6cb65233ecf8f22989c97fa"
}
],
"dirSha256": "54232ed2470330f615270dbd71501fa94b2ef899e22d45294cf8d10cb4981af0"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,102 @@
---
name: worktree-task
description: Manage large coding tasks using git worktrees and background Claude Code sessions. Use this when users want to execute large, complex tasks (like implementing a new service, major refactoring, or multi-step features) in isolation without blocking the current session. Spawns autonomous Claude Code instances via tmux.
---
# Worktree Task Manager
This skill manages large coding tasks by spawning autonomous Claude Code instances in separate git worktrees via tmux sessions.
## When to Use
- User wants to execute a large task (>20 subtasks) without blocking current session
- User mentions "background", "parallel", "worktree", or "autonomous" execution
- Task involves creating a new service, major refactoring, or implementing complex features
- User wants to continue other work while a large task runs
## Available Commands
Use these slash commands for precise control:
| Command | Description |
|---------|-------------|
| `/worktree:launch` | Launch a new background task |
| `/worktree:status` | Check status of all or specific tasks |
| `/worktree:resume` | Resume an interrupted task |
| `/worktree:cleanup` | Clean up completed tasks |
## Core Workflow
### 1. Launch a Task
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/launch.py <branch-name> "<task-description>"
```
Example:
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/launch.py feature/my-task "Execute the task: implement new feature. Read the tasks.md and implement all phases."
```
The script will:
1. Verify git status is clean (or prompt to commit/stash)
2. Create a git worktree with the specified branch
3. Create a tmux session
4. Launch Claude Code with `--dangerously-skip-permissions`
5. Send the task with instructions to use Task tool for each phase
### 2. Monitor Progress
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/status.py [session-name]
```
Without arguments, lists all active sessions. With a session name, shows detailed status.
### 3. Resume an Interrupted Task
If a task is interrupted (rate limit, API error, timeout):
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/resume.py <session-name> [message]
```
Options:
- `--retry` - Retry the last failed task
- `--check` - Only check status, don't send message
### 4. Cleanup
```bash
python3 ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup.py <session-name> [--remove-worktree]
```
## Alerts
This plugin automatically sends macOS notifications when:
- A background task completes successfully
- A task encounters an error (rate limit, API error)
- A session ends
Configure alerts in `hooks/hooks.json`.
## Critical Instructions for Spawned Claude
The spawned Claude Code instance receives these critical instructions:
1. **MUST use Task tool** - Each major phase must be executed via `Task` tool to prevent context overflow
2. **Silent mode** - No confirmations needed, user has pre-approved all operations
3. **Complete execution** - Do not stop until all tasks are done
4. **Track with TodoWrite** - Create and update todo list for visibility
5. **Commit often** - Make atomic commits after each logical unit
## Notes
- Worktrees are created in parent directory: `../<project>-<branch-name>`
- tmux session names have `/` and `.` replaced with `-`
- Use `tmux attach -t <session>` to take over interactively
- The spawned Claude runs with full permissions (`--dangerously-skip-permissions`)
---
> 💡 More Claude Code plugins: [github.com/ourines](https://github.com/ourines)