From 63b89eed6d4c9c847c1f0353e94f1a98a80133a0 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:59:41 +0000 Subject: [PATCH] feat: add opencode-worktree skill for isolated sessions - Adds skills/opencode-worktree/ with SKILL.md and opencode-worktree.sh - Creates unique git worktree per session (e.g. session-20260327-a1b2c3-refactor-auth) - Cleans up stale worktrees on every launch - Branch always based on main - User can source directly or copy to PATH --- skills/opencode-worktree/SKILL.md | 215 ++++++++++++++++++ skills/opencode-worktree/opencode-worktree.sh | 146 ++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 skills/opencode-worktree/SKILL.md create mode 100644 skills/opencode-worktree/opencode-worktree.sh diff --git a/skills/opencode-worktree/SKILL.md b/skills/opencode-worktree/SKILL.md new file mode 100644 index 0000000..7066ff0 --- /dev/null +++ b/skills/opencode-worktree/SKILL.md @@ -0,0 +1,215 @@ +# opencode-worktree + +Isolated OpenCode sessions via git worktrees. + +## Overview + +Each OpenCode session gets its own git worktree with a unique branch. This prevents: +- Clashes with parallel sessions on the same repo +- Accidental overwrites from multiple agents +- Confusion from work-in-progress across contexts + +## Prerequisites + +- Git +- opencode installed and configured + +## Installation + +### Option 1: Source directly (Recommended) +```bash +. skills/opencode-worktree/opencode-worktree.sh +``` + +### Option 2: Copy to PATH +```bash +cp skills/opencode-worktree/opencode-worktree.sh ~/.local/bin/opencode-worktree +chmod +x ~/.local/bin/opencode-worktree +``` + +## Usage + +### Create new session +```bash +. opencode-worktree.sh # session-20260327-a1b2c3 +. opencode-worktree.sh refactor-auth # session-20260327-a1b2c3-refactor-auth +``` + +### Cleanup +```bash +. opencode-worktree.sh --cleanup # remove all session-* worktrees +. opencode-worktree.sh --cleanup # remove specific worktree +``` + +## How It Works + +1. **Cleanup** - On every launch, removes all stale `session-*` worktrees and their branches +2. **Create** - Creates new worktree based on `main` with unique name: `session-{timestamp}-{random6}[-{purpose}]` +3. **Launch** - Changes into worktree and launches opencode +4. **Exit** - When opencode exits, you return to your original directory (worktree remains for review) + +## Example Workflow + +```bash +# Start session for refactoring auth +. opencode-worktree.sh refactor-auth + +# ... do work in opencode ... + +# Exit opencode (worktree with your changes still exists) +# Later, resume or cleanup +. opencode-worktree.sh --cleanup session-20260327-a1b2c3-refactor-auth +``` + +--- + +## Script Source + +```bash +#!/bin/bash +# opencode-worktree - Isolated OpenCode sessions via git worktrees + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKTREE_BASE="$PWD/.git/worktrees" +PURPOSE="" +CLEANUP_ONLY=false +CLEANUP_NAME="" + +usage() { + cat < Remove specific worktree by name + +Examples: + $(basename "$0") # session-20260327-a1b2c3 + $(basename "$0") refactor-auth # session-20260327-a1b2c3-refactor-auth + $(basename "$0") --cleanup # remove all session-* worktrees +EOF +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --cleanup) + CLEANUP_ONLY=true + if [[ $# -gt 1 && ! "$2" =~ ^-- ]]; then + CLEANUP_NAME="$2" + shift + fi + ;; + -h|--help) + usage + exit 0 + ;; + *) + if [[ -z "$PURPOSE" ]]; then + PURPOSE="$1" + fi + ;; + esac + shift + done +} + +cleanup_stale() { + if [[ ! -d "$WORKTREE_BASE" ]]; then + return + fi + + for wt in "$WORKTREE_BASE"/session-*; do + [[ -d "$wt" ]] || continue + + branch=$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null) || continue + + echo "Removing stale worktree: $(basename "$wt")" + git worktree remove "$wt" --force 2>/dev/null || true + if [[ -n "$branch" && "$branch" != "HEAD" ]]; then + git branch -D "$branch" 2>/dev/null || true + fi + done +} + +cleanup_single() { + local name="$1" + local wt_path="$WORKTREE_BASE/$name" + + if [[ ! -d "$wt_path" ]]; then + echo "Worktree '$name' not found" + return + fi + + branch=$(git -C "$wt_path" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch="" + + echo "Removing worktree: $name" + git worktree remove "$wt_path" --force 2>/dev/null || true + if [[ -n "$branch" && "$branch" != "HEAD" ]]; then + git branch -D "$branch" 2>/dev/null || true + fi +} + +create_worktree() { + local timestamp=$(date +%Y%m%d-%H%M%S) + local random=$(head -c 3 /dev/urandom | xxd -p | head -c 6) + local worktree_name="session-${timestamp}-${random}" + local branch_name="$worktree_name" + + if [[ -n "$PURPOSE" ]]; then + worktree_name="${worktree_name}-${PURPOSE}" + branch_name="$worktree_name" + fi + + local worktree_path="$WORKTREE_BASE/$worktree_name" + + # Cleanup any existing with same name + if [[ -d "$worktree_path" ]]; then + echo "Removing existing worktree: $worktree_name" + git worktree remove "$worktree_path" --force 2>/dev/null || true + fi + + # Ensure main exists and is up to date + if ! git show-ref --quiet refs/heads/main 2>/dev/null; then + echo "Error: 'main' branch does not exist" + exit 1 + fi + + # Create worktree from main + echo "Creating worktree: $worktree_name" + git worktree add -b "$branch_name" "$worktree_path" main + + # Launch opencode in worktree + echo "Entering worktree and launching opencode..." + cd "$worktree_path" + exec opencode +} + +main() { + # Verify we're in a git repo + if ! git rev-parse --is-inside-work-tree 2>/dev/null; then + echo "Error: Must be run inside a git repository" + exit 1 + fi + + if [[ "$CLEANUP_ONLY" == true ]]; then + if [[ -n "$CLEANUP_NAME" ]]; then + cleanup_single "$CLEANUP_NAME" + else + cleanup_stale + fi + exit 0 + fi + + cleanup_stale + create_worktree +} + +parse_args "$@" +main +``` \ No newline at end of file diff --git a/skills/opencode-worktree/opencode-worktree.sh b/skills/opencode-worktree/opencode-worktree.sh new file mode 100644 index 0000000..cb0065c --- /dev/null +++ b/skills/opencode-worktree/opencode-worktree.sh @@ -0,0 +1,146 @@ +#!/bin/bash +# opencode-worktree - Isolated OpenCode sessions via git worktrees + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKTREE_BASE="$PWD/.git/worktrees" +PURPOSE="" +CLEANUP_ONLY=false +CLEANUP_NAME="" + +usage() { + cat < Remove specific worktree by name + +Examples: + $(basename "$0") # session-20260327-a1b2c3 + $(basename "$0") refactor-auth # session-20260327-a1b2c3-refactor-auth + $(basename "$0") --cleanup # remove all session-* worktrees +EOF +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --cleanup) + CLEANUP_ONLY=true + if [[ $# -gt 1 && ! "$2" =~ ^-- ]]; then + CLEANUP_NAME="$2" + shift + fi + ;; + -h|--help) + usage + exit 0 + ;; + *) + if [[ -z "$PURPOSE" ]]; then + PURPOSE="$1" + fi + ;; + esac + shift + done +} + +cleanup_stale() { + if [[ ! -d "$WORKTREE_BASE" ]]; then + return + fi + + for wt in "$WORKTREE_BASE"/session-*; do + [[ -d "$wt" ]] || continue + + branch=$(git -C "$wt" rev-parse --abbrev-ref HEAD 2>/dev/null) || continue + + echo "Removing stale worktree: $(basename "$wt")" + git worktree remove "$wt" --force 2>/dev/null || true + if [[ -n "$branch" && "$branch" != "HEAD" ]]; then + git branch -D "$branch" 2>/dev/null || true + fi + done +} + +cleanup_single() { + local name="$1" + local wt_path="$WORKTREE_BASE/$name" + + if [[ ! -d "$wt_path" ]]; then + echo "Worktree '$name' not found" + return + fi + + branch=$(git -C "$wt_path" rev-parse --abbrev-ref HEAD 2>/dev/null) || branch="" + + echo "Removing worktree: $name" + git worktree remove "$wt_path" --force 2>/dev/null || true + if [[ -n "$branch" && "$branch" != "HEAD" ]]; then + git branch -D "$branch" 2>/dev/null || true + fi +} + +create_worktree() { + local timestamp=$(date +%Y%m%d-%H%M%S) + local random=$(head -c 3 /dev/urandom | xxd -p | head -c 6) + local worktree_name="session-${timestamp}-${random}" + local branch_name="$worktree_name" + + if [[ -n "$PURPOSE" ]]; then + worktree_name="${worktree_name}-${PURPOSE}" + branch_name="$worktree_name" + fi + + local worktree_path="$WORKTREE_BASE/$worktree_name" + + # Cleanup any existing with same name + if [[ -d "$worktree_path" ]]; then + echo "Removing existing worktree: $worktree_name" + git worktree remove "$worktree_path" --force 2>/dev/null || true + fi + + # Ensure main exists and is up to date + if ! git show-ref --quiet refs/heads/main 2>/dev/null; then + echo "Error: 'main' branch does not exist" + exit 1 + fi + + # Create worktree from main + echo "Creating worktree: $worktree_name" + git worktree add -b "$branch_name" "$worktree_path" main + + # Launch opencode in worktree + echo "Entering worktree and launching opencode..." + cd "$worktree_path" + exec opencode +} + +main() { + # Verify we're in a git repo + if ! git rev-parse --is-inside-work-tree 2>/dev/null; then + echo "Error: Must be run inside a git repository" + exit 1 + fi + + if [[ "$CLEANUP_ONLY" == true ]]; then + if [[ -n "$CLEANUP_NAME" ]]; then + cleanup_single "$CLEANUP_NAME" + else + cleanup_stale + fi + exit 0 + fi + + cleanup_stale + create_worktree +} + +parse_args "$@" +main \ No newline at end of file