From solopreneur
Creates a git worktree for a task and records handoff context into a committed plan file. Useful when switching tasks or handing off to a fresh session with full context.
How this skill is triggered — by the user, by Claude, or both
Slash command
/solopreneur:worktree-handoffThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Create an isolated workspace for a new or in-progress task, and record the complete
Create an isolated workspace for a new or in-progress task, and record the complete task context into a plan file that gets committed to the branch. The next session reads the plan file to pick up without re-explanation.
Inline the cascade config helpers, then determine the operating mode:
# --- solopreneur config helpers (inlined from shared/config.md) ---
# Compute the canonical repo identity used as the key under `.repos` in
# solopreneur.json. Falls back to git toplevel path, then $PWD.
solopreneur_repo_key() {
local url root
url=$(git remote get-url origin 2>/dev/null || true)
if [ -n "$url" ]; then
# Strip protocol schemes (https/http/ssh/git) and user prefixes (git@)
# in either order — origin URLs come in many shapes:
# https://github.com/owner/repo.git
# http://github.com/owner/repo.git
# ssh://[email protected]/owner/repo.git
# git://github.com/owner/repo.git
# [email protected]:owner/repo.git
url="${url#https://}"; url="${url#http://}"
url="${url#ssh://}"; url="${url#git://}"
url="${url#git@}"
url="${url%.git}"
# Replace the first `:` with `/` — the scp-style `git@host:owner/repo`
# form. Bash `${var/pattern/replacement}` parses the second `/` as the
# delimiter; the chars after it (`/` here) are the replacement, so this
# produces a single slash, not double. (Tested.)
url="${url/://}"
printf '%s\n' "$url"
return
fi
root=$(git rev-parse --show-toplevel 2>/dev/null || true)
if [ -n "$root" ]; then
printf '%s\n' "$root"
return
fi
printf '%s\n' "$PWD"
}
# Read a feature subtree from solopreneur.json with the 5-layer cascade:
# 1. primary .repos[<repo-key>].<feature>
# 2. primary .default.<feature>
# 3. fallback .repos[<repo-key>].<feature>
# 4. fallback .default.<feature>
# 5. legacy top-level .<feature> (primary then fallback)
# First non-null wins. Each layer is checked inline (no nested helper
# function — bash function declarations are global, even nested ones, and
# would pollute the user's shell namespace).
read_solopreneur_config() {
local key="\$1"
local primary="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/solopreneur.json"
local fallback="$HOME/.claude/solopreneur.json"
local repo_key; repo_key=$(solopreneur_repo_key)
local out
# Layer 1: primary .repos[<repo-key>].<feature>
if [ -f "$primary" ]; then
out=$(jq -r --arg rk "$repo_key" --arg fk "$key" '.repos[$rk][$fk] | values' "$primary" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; return; fi
# Layer 2: primary .default.<feature>
out=$(jq -r --arg fk "$key" '.default[$fk] | values' "$primary" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; return; fi
fi
# Layers 3 + 4: fallback file, only if different from primary
if [ "$primary" != "$fallback" ] && [ -f "$fallback" ]; then
out=$(jq -r --arg rk "$repo_key" --arg fk "$key" '.repos[$rk][$fk] | values' "$fallback" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; return; fi
out=$(jq -r --arg fk "$key" '.default[$fk] | values' "$fallback" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; return; fi
fi
# Layer 5: legacy top-level — primary then fallback
if [ -f "$primary" ]; then
out=$(jq -r --arg fk "$key" '.[$fk] | values' "$primary" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; return; fi
fi
if [ "$primary" != "$fallback" ] && [ -f "$fallback" ]; then
out=$(jq -r --arg fk "$key" '.[$fk] | values' "$fallback" 2>/dev/null)
if [ -n "$out" ]; then printf '%s\n' "$out"; return; fi
fi
}
# Write a feature subtree to .default.<key> in the primary file.
# Sibling keys are preserved (atomic read-modify-write).
# Usage: write_solopreneur_config greenlight '{fallback_order:["codex-bot","gemini"]}'
write_solopreneur_config() {
local key="\$1"
local value_expr="\$2"
local primary="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/solopreneur.json"
local tmp existing
mkdir -p "$(dirname "$primary")"
tmp=$(mktemp "${primary}.XXXXXX")
existing=$(cat "$primary" 2>/dev/null); [ -z "$existing" ] && existing='{}'
printf '%s\n' "$existing" \
| jq --arg fk "$key" --argjson v "$(jq -n "$value_expr")" \
'.default = ((.default // {}) | .[$fk] = $v)' \
> "$tmp" || { rm -f "$tmp"; return 1; }
mv "$tmp" "$primary"
}
# Write a feature subtree to .repos[<repo-key>].<key> in the primary file.
# Sibling repos AND sibling features within the same repo are preserved.
# Usage: write_solopreneur_repo_config preview '{path:"docs/preview"}'
write_solopreneur_repo_config() {
local key="\$1"
local value_expr="\$2"
local primary="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/solopreneur.json"
local repo_key; repo_key=$(solopreneur_repo_key)
local tmp existing
mkdir -p "$(dirname "$primary")"
tmp=$(mktemp "${primary}.XXXXXX")
existing=$(cat "$primary" 2>/dev/null); [ -z "$existing" ] && existing='{}'
printf '%s\n' "$existing" \
| jq --arg rk "$repo_key" --arg fk "$key" --argjson v "$(jq -n "$value_expr")" \
'.repos = ((.repos // {}) | .[$rk] = ((.[$rk] // {}) | .[$fk] = $v))' \
> "$tmp" || { rm -f "$tmp"; return 1; }
mv "$tmp" "$primary"
}
# --- end solopreneur config helpers ---
TODOS_CONFIG=$(read_solopreneur_config todos)
PLANS_CONFIG=$(read_solopreneur_config plans)
BACKLOG=$(echo "${TODOS_CONFIG:-{}}" | jq -r '.backlog // empty')
DOING=$(echo "${TODOS_CONFIG:-{}}" | jq -r '.doing // empty')
PLANS_DIR=$(echo "${PLANS_CONFIG:-{}}" | jq -r '.dir // empty')
PLANS_DIR="${PLANS_DIR:-docs/solopreneur/plans}" # default
MODE=$([ -n "$BACKLOG" ] && [ -n "$DOING" ] && echo "state-machine" || echo "flat")
Mode decision:
$BACKLOG and $DOING are both non-empty$PLANS_DIRName based on task type and description:
| Task Type | Branch Prefix | Example |
|---|---|---|
| Bug fix | fix/ | fix/sleep-calculation |
| New feature | feature/ | feature/health-connect-edit |
| Refactor | refactor/ | refactor/sleep-sessionizer |
| Research | research/ | research/healthkit-sources |
Worktree directory name = branch name slug (strip prefix, use - separators).
Always place worktrees under .worktrees/:
git worktree add .worktrees/<slug> -b <branch-name>
Prohibited: operating on main, placing worktrees outside .worktrees/.
Worktrees are separate directories — gitignored files don't carry over automatically.
Check .gitignore for config/secret patterns, find matching files in the main repo,
copy them to the same relative paths in the worktree. Never copy build artifacts.
MAIN_REPO="$(git worktree list | head -1 | awk '{print \$1}')"
WORKTREE=".worktrees/<slug>"
# Find gitignored config files that exist in the main repo
# Common patterns: .env, *.xcconfig, local.properties, secrets.json
# Copy any that are relevant to this project to the worktree's corresponding paths
Collect all plan files:
# State-machine mode: collect from backlog + doing
if [ "$MODE" = "state-machine" ]; then
ALL_FILES=$(find "$BACKLOG" "$DOING" -maxdepth 1 -name "*.md" 2>/dev/null)
else
mkdir -p "$PLANS_DIR"
ALL_FILES=$(find "$PLANS_DIR" -maxdepth 1 -name "*.md" 2>/dev/null)
fi
Context matching — before prompting the user:
Extract 3–5 key terms from the current conversation (task name, feature area, keywords from the user's description). Then score each plan file:
Plan-Branch: lines, first 20 lines) for the key terms# ILLUSTRATIVE ONLY — substitute actual key terms extracted from the current
# conversation before running. Do NOT execute this block with these hardcoded terms.
TERMS="<term1>\|<term2>\|<term3>" # replace with actual key terms
CANDIDATES=""
while IFS= read -r f; do
[ -z "$f" ] && continue
score=$(head -20 "$f" | grep -i -c "$TERMS" 2>/dev/null); score=${score:-0}
name_score=$(basename "$f" | grep -i -c "$TERMS" 2>/dev/null); name_score=${name_score:-0}
total=$((score + name_score * 2))
[ "$total" -gt 0 ] && CANDIDATES="$CANDIDATES\n$total $f"
done <<< "$ALL_FILES"
CANDIDATES=$(printf '%b' "$CANDIDATES" | sort -rn)
MATCH_COUNT=$([ -n "$CANDIDATES" ] && printf '%b' "$CANDIDATES" | grep -c . || echo 0)
Match count = number of files with a score greater than zero (i.e., at least one key term found in the filename or first 20 lines of content).
Decision based on match count:
0 matches — if $ALL_FILES is non-empty, briefly tell the user: "No matching plans found for this task — creating a new plan file. (Reply with a filename to use an existing plan instead.)" Then proceed to Step 6 to create a new file.
1 match — use it automatically, no prompt. Print: → Matched plan: <filename> so the user knows what was selected.
2+ matches — show only the matching candidates (not all files) with index numbers, then ask:
"Found N matching plans. Which one does this worktree belong to? Enter a number to reuse it, or 'new' to create a fresh plan file."
In state-machine mode, note the source directory in parentheses (Backlog / Doing) next to each filename.
If no plan files exist at all, skip all of the above and proceed directly to creating a new file.
Recording the source: When a file is selected, set PLAN_SOURCE=backlog or
PLAN_SOURCE=doing based on which directory it came from. Step 6 checks
$PLAN_SOURCE to decide whether to run git mv (only when PLAN_SOURCE=backlog).
Create a new file:
<doing>/<YYYY-MM-DD>-<branch-slug>.md<plans-dir>/<YYYY-MM-DD>-<branch-slug>.mdWhere <branch-slug> = branch name with / replaced by -
(e.g., fix/sleep-calculation → fix-sleep-calculation).
Date from $(date +%Y-%m-%d).
New file content:
<!--
Plan-Branch: <branch-name>
-->
## Handoff Context (<YYYY-MM-DD>, branch: <branch-name>)
### Problem Background
<why is this being done — user reports, screenshots, logs>
### Root Cause
<known technical issues, with file paths + line numbers>
### Items to Fix / Implement
- [ ] <item with expected approach>
### Key Files
| path | description |
|------|-------------|
### Current Progress
Not started.
Fill in the five sections based on context from the current conversation.
Move from backlog to doing (state-machine mode only):
If the selected file is in $BACKLOG, move it:
git mv "$BACKLOG/<filename>" "$DOING/<filename>"
PLAN_FILE="$DOING/<filename>"
In flat mode, skip this step — the file stays in place.
Ensure the Plan-Branch: marker block exists at the top of the file.
If the file already starts with an HTML comment block containing at least one
Plan-Branch: line, check whether Plan-Branch: <branch-name> is already
present. If it is absent, append the line inside the existing comment block:
python3 -c "
import sys branch, path = sys.argv[1], sys.argv[2] with open(path) as f: content = f.read() idx = content.find('\n-->') if idx != -1: content = content[:idx+1] + 'Plan-Branch: ' + branch + '\n' + content[idx+1:] with open(path, 'w') as f: f.write(content) " "" "$PLAN_FILE"
If no comment block exists at the top (legacy file), prepend one:
```markdown
<!--
Plan-Branch: <branch-name>
-->
Append the handoff section at the end of the file:
## Handoff Context (<YYYY-MM-DD>, branch: <branch-name>)
### Problem Background
<why is this being done — user reports, screenshots, logs>
### Root Cause
<known technical issues, with file paths + line numbers>
### Items to Fix / Implement
- [ ] <item with expected approach>
### Key Files
| path | description |
|------|-------------|
### Current Progress
<not started / steps done so far>
Fill in the five sections based on context from the current conversation. Be specific: include file names, line numbers, error messages, actual numbers. Include root cause analysis already done so the next session doesn't debug from scratch. If the conversation had screenshots or user-reported bug details, include everything.
git add <plan-file-path>
# Also stage the git mv result if the file moved from backlog to doing
git commit -m "docs(handoff): context for <branch-name>"
git push -u origin <branch-name>
Single commit. Push immediately so the doc is visible to the next session and to PR reviewers.
Print this so the user can paste it into a new session:
cd /absolute/path/to/repo/.worktrees/<slug>
Plan file: <relative/path/to/plan.md>
Read the plan file for the full context — branch <branch-name> is tracked under
the `Plan-Branch:` marker, and the latest `## Handoff Context` section captures
the current state.
Branch: <branch-name>, <one-line task description>.
Use an absolute path in the cd command (from git worktree list). The plan file
path should be relative to the repo root.
User says: "Open a worktree to fix the sleep calculation — the issue is duplicate sources from HealthKit"
git worktree add .worktrees/fix-sleep-calculation -b fix/sleep-calculation
Plan file discovery: user picks existing 2026-04-10-sleep-tracking.md from
backlog (state-machine mode). File is git mv'd to doing, Plan-Branch: fix/sleep-calculation is added to the marker block, and the Handoff Context
section is appended with the HealthKit duplicate-source root cause detail.
Commit: docs(handoff): context for fix/sleep-calculation
Output for user:
cd /path/to/project/.worktrees/fix-sleep-calculation
Plan file: docs/solopreneur/plans/doing/2026-04-10-sleep-tracking.md
Read the plan file for the full context — branch fix/sleep-calculation is tracked
under the `Plan-Branch:` marker, and the latest `## Handoff Context` section
captures the current state.
Branch: fix/sleep-calculation, deduplicate HealthKit multi-source sleep samples
so displayed sleep time matches Apple Health's value.
npx claudepluginhub p/hanamizuki-solopreneur-plugins-solopreneurManages git worktrees for parallel feature development: create, list, switch between, and merge worktrees with isolated task lists.
Creates isolated git worktrees for parallel development without disrupting the main workspace. Includes safety verification to prevent accidental commits of worktree contents.
Creates a git worktree for isolated parallel development — new branch in a separate directory with project setup and test baseline. Enables multiple Claude Code sessions on different tasks simultaneously.