Initial commit
This commit is contained in:
17
.claude-plugin/plugin.json
Normal file
17
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
47
commands/cleanup.md
Normal 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
69
commands/help.md
Normal 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
42
commands/launch.md
Normal 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
44
commands/merge.md
Normal 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
44
commands/rebase.md
Normal 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
51
commands/resume.md
Normal 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
43
commands/status.md
Normal 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
143
hooks/handlers/on-session-end.py
Executable 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()
|
||||
467
hooks/handlers/on-session-start.py
Executable file
467
hooks/handlers/on-session-start.py
Executable 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
152
hooks/handlers/on-stop.py
Executable 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
37
hooks/hooks.json
Normal 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
89
plugin.lock.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
102
skills/worktree-task/SKILL.md
Normal file
102
skills/worktree-task/SKILL.md
Normal 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)
|
||||
Reference in New Issue
Block a user