Project Session Manager (PSM) Skill is the compatibility alias for this canonical skill entrypoint. Quick Start (worktree-first): Start with when you want an isolated issue/PR/feature worktree before adding any tmux/session orchestration: See Teleport Command below for details. Automate isolated development environments using git worktrees and tmux sessions with Claude Code. Enables parallel work across multiple tasks, projects, and repositories. Canonical slash command: (alias: ). Commands | Command | Description | Example | |---------|-------------|---------| | | PR review session | | | | I…

\n}\n\n# Poll the tmux pane until Claude's REPL prompt appears.\n# Usage: psm_wait_for_claude_prompt \u003csession_name> [max_seconds]\n# Returns 0 when prompt detected; 1 on timeout.\npsm_wait_for_claude_prompt() {\n local session_name\n session_name=$(psm_tmux_safe_name \"$1\")\n local max_wait=\"${2:-30}\"\n local waited=0\n\n while [[ $waited -lt $max_wait ]]; do\n local pane_content\n pane_content=$(tmux capture-pane -t \"$session_name\" -p 2>/dev/null) || return 1\n if _psm_pane_has_claude_prompt \"$pane_content\"; then\n return 0\n fi\n sleep 1\n (( waited++ )) || true\n done\n\n return 1\n}\n\n# Wait for Claude's REPL to be ready then inject a context-file trigger prompt.\n# Non-fatal: warns on timeout but does not fail the session creation.\n# Usage: psm_inject_prompt \u003csession_name> \u003ccontext_file_relative_path>\npsm_inject_prompt() {\n local session_name\n session_name=$(psm_tmux_safe_name \"$1\")\n local context_file=\"$2\"\n\n if ! psm_wait_for_claude_prompt \"$session_name\"; then\n echo \"warn|Timed out waiting for Claude prompt; task context not injected\" >&2\n return 0\n fi\n\n local trigger=\"Read ${context_file} for full task context, then begin.\"\n\n # Use literal mode (-l) to prevent tmux from interpreting key names in the text\n tmux send-keys -t \"$session_name\" -l -- \"$trigger\"\n sleep 0.15\n tmux send-keys -t \"$session_name\" Enter\n\n return 0\n}\n\n# Kill a tmux session\n# Usage: psm_kill_tmux_session \u003csession_name>\npsm_kill_tmux_session() {\n local session_name\n session_name=$(psm_tmux_safe_name \"$1\")\n\n if ! tmux has-session -t \"$session_name\" 2>/dev/null; then\n echo \"not_found|$session_name\"\n return 0\n fi\n\n tmux kill-session -t \"$session_name\" 2>/dev/null || {\n echo \"error|Failed to kill session\"\n return 1\n }\n\n echo \"killed|$session_name\"\n return 0\n}\n\n# List all PSM tmux sessions\npsm_list_tmux_sessions() {\n if ! psm_has_tmux; then\n return 0\n fi\n\n tmux list-sessions -F \"#{session_name}|#{session_created}|#{session_attached}\" 2>/dev/null | grep \"^psm_\" || true\n}\n\n# Check if a tmux session exists\n# Usage: psm_tmux_session_exists \u003csession_name>\npsm_tmux_session_exists() {\n local session_name\n session_name=$(psm_tmux_safe_name \"$1\")\n tmux has-session -t \"$session_name\" 2>/dev/null\n}\n\n# Get current tmux session name\npsm_current_tmux_session() {\n if [[ -n \"$TMUX\" ]]; then\n tmux display-message -p \"#{session_name}\" 2>/dev/null\n fi\n}\n\n# Generate tmux session name\n# Usage: psm_tmux_session_name \u003calias> \u003ctype> \u003cid>\npsm_tmux_session_name() {\n local alias=\"$1\"\n local type=\"$2\"\n local id=\"$3\"\n\n psm_tmux_name_from_id \"${alias}:${type}-${id}\"\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":7912,"content_sha256":"c4bde46612f407e30094f2df46465ebeb0afb182499de4ca27b1a47358603d64"},{"filename":"lib/worktree.sh","content":"#!/usr/bin/env bash\n# PSM Worktree Management\n\n# Validate worktree path is under PSM worktree root before deletion\n# Returns 0 if valid, 1 if invalid\n# Usage: validate_worktree_path \u003cpath>\nvalidate_worktree_path() {\n local path=\"$1\"\n local worktree_root\n worktree_root=$(psm_get_worktree_root 2>/dev/null) || return 1\n\n # Path must exist and be a directory\n if [[ ! -d \"$path\" ]]; then\n return 1\n fi\n\n # Resolve to absolute paths for comparison\n local abs_path abs_root\n abs_path=$(cd \"$path\" 2>/dev/null && pwd) || return 1\n abs_root=$(cd \"$worktree_root\" 2>/dev/null && pwd) || return 1\n\n # Check path is under root and doesn't contain ..\n if [[ \"$abs_path\" != \"$abs_root\"/* ]] || [[ \"$path\" == *\"..\"* ]]; then\n echo \"error|Invalid worktree path: not under PSM root\" >&2\n return 1\n fi\n return 0\n}\n\n# Best-effort dependency bootstrap for clean PR review worktrees.\n# Reuses the source repo node_modules only when package.json matches exactly.\n# This keeps focused test commands like `npm run test:run -- ...` usable\n# without forcing a full install in every review worktree.\npsm_bootstrap_review_dependencies() {\n local local_repo=\"$1\"\n local worktree_path=\"$2\"\n\n local source_package_json=\"${local_repo}/package.json\"\n local target_package_json=\"${worktree_path}/package.json\"\n local source_node_modules=\"${local_repo}/node_modules\"\n local target_node_modules=\"${worktree_path}/node_modules\"\n\n [[ -f \"$source_package_json\" ]] || return 0\n [[ -f \"$target_package_json\" ]] || return 0\n [[ -d \"$source_node_modules\" ]] || return 0\n [[ ! -e \"$target_node_modules\" ]] || return 0\n\n cmp -s \"$source_package_json\" \"$target_package_json\" || return 0\n\n ln -s \"$source_node_modules\" \"$target_node_modules\" 2>/dev/null || true\n return 0\n}\n\n# Create a worktree for PR review\n# Usage: psm_create_pr_worktree \u003clocal_repo> \u003calias> \u003cpr_number> \u003cpr_branch>\npsm_create_pr_worktree() {\n local local_repo=\"$1\"\n local alias=\"$2\"\n local pr_number=\"$3\"\n local pr_branch=\"$4\"\n\n local worktree_root=$(psm_get_worktree_root)\n local worktree_path=\"${worktree_root}/${alias}/pr-${pr_number}\"\n\n # Check if worktree already exists\n if [[ -d \"$worktree_path\" ]]; then\n echo \"exists|$worktree_path\"\n return 1\n fi\n\n # Ensure parent directory exists\n mkdir -p \"${worktree_root}/${alias}\"\n\n # Fetch the PR branch\n cd \"$local_repo\" || return 1\n git fetch origin \"pull/${pr_number}/head:psm-pr-${pr_number}-review\" >/dev/null 2>&1 || {\n echo \"error|Failed to fetch PR #${pr_number}\"\n return 1\n }\n\n # Create worktree\n git worktree add \"$worktree_path\" \"psm-pr-${pr_number}-review\" >/dev/null 2>&1 || {\n echo \"error|Failed to create worktree\"\n return 1\n }\n\n psm_bootstrap_review_dependencies \"$local_repo\" \"$worktree_path\"\n\n echo \"created|$worktree_path\"\n return 0\n}\n\n# Create a worktree for issue fix\n# Usage: psm_create_issue_worktree \u003clocal_repo> \u003calias> \u003cissue_number> \u003cslug> \u003cbase_branch>\npsm_create_issue_worktree() {\n local local_repo=\"$1\"\n local alias=\"$2\"\n local issue_number=\"$3\"\n local slug=\"$4\"\n local base_branch=\"${5:-main}\"\n\n local worktree_root=$(psm_get_worktree_root)\n local worktree_path=\"${worktree_root}/${alias}/issue-${issue_number}\"\n local branch_name=\"fix/${issue_number}-${slug}\"\n\n # Check if worktree already exists\n if [[ -d \"$worktree_path\" ]]; then\n echo \"exists|$worktree_path|$branch_name\"\n return 1\n fi\n\n mkdir -p \"${worktree_root}/${alias}\"\n\n cd \"$local_repo\" || return 1\n\n # Fetch latest from origin\n git fetch origin \"$base_branch\" >/dev/null 2>&1 || {\n echo \"error|Failed to fetch $base_branch\"\n return 1\n }\n\n # Create and checkout new branch\n git branch \"$branch_name\" \"origin/$base_branch\" >/dev/null 2>&1 || {\n # Branch might already exist\n true\n }\n\n # Create worktree\n git worktree add \"$worktree_path\" \"$branch_name\" >/dev/null 2>&1 || {\n echo \"error|Failed to create worktree\"\n return 1\n }\n\n echo \"created|$worktree_path|$branch_name\"\n return 0\n}\n\n# Create a worktree for feature development\n# Usage: psm_create_feature_worktree \u003clocal_repo> \u003calias> \u003cfeature_name> \u003cbase_branch>\npsm_create_feature_worktree() {\n local local_repo=\"$1\"\n local alias=\"$2\"\n local feature_name=\"$3\"\n local base_branch=\"${4:-main}\"\n\n local worktree_root=$(psm_get_worktree_root)\n local safe_name=$(psm_sanitize \"$feature_name\")\n local worktree_path=\"${worktree_root}/${alias}/feat-${safe_name}\"\n local branch_name=\"feature/${safe_name}\"\n\n # Check if worktree already exists\n if [[ -d \"$worktree_path\" ]]; then\n echo \"exists|$worktree_path|$branch_name\"\n return 1\n fi\n\n mkdir -p \"${worktree_root}/${alias}\"\n\n cd \"$local_repo\" || return 1\n\n # Fetch latest\n git fetch origin \"$base_branch\" >/dev/null 2>&1 || {\n echo \"error|Failed to fetch $base_branch\"\n return 1\n }\n\n # Create branch\n git branch \"$branch_name\" \"origin/$base_branch\" >/dev/null 2>&1 || true\n\n # Create worktree\n git worktree add \"$worktree_path\" \"$branch_name\" >/dev/null 2>&1 || {\n echo \"error|Failed to create worktree\"\n return 1\n }\n\n echo \"created|$worktree_path|$branch_name\"\n return 0\n}\n\n# Remove a worktree\n# Usage: psm_remove_worktree \u003clocal_repo> \u003cworktree_path>\npsm_remove_worktree() {\n local local_repo=\"$1\"\n local worktree_path=\"$2\"\n\n if [[ ! -d \"$worktree_path\" ]]; then\n echo \"not_found|$worktree_path\"\n return 1\n fi\n\n # Check for uncommitted changes\n if [[ -d \"$worktree_path/.git\" ]] || [[ -f \"$worktree_path/.git\" ]]; then\n cd \"$worktree_path\" || return 1\n if [[ -n $(git status --porcelain 2>/dev/null) ]]; then\n echo \"dirty|$worktree_path\"\n return 1\n fi\n fi\n\n cd \"$local_repo\" || return 1\n\n # Validate path is under PSM worktree root before any deletion\n if validate_worktree_path \"$worktree_path\"; then\n git worktree remove \"$worktree_path\" --force 2>/dev/null || {\n # Force remove the directory if git worktree remove fails\n rm -rf \"$worktree_path\"\n }\n else\n echo \"error|Refusing to delete path outside worktree root: $worktree_path\" >&2\n return 1\n fi\n\n echo \"removed|$worktree_path\"\n return 0\n}\n\n# List all PSM worktrees\npsm_list_worktrees() {\n local worktree_root=$(psm_get_worktree_root)\n\n if [[ ! -d \"$worktree_root\" ]]; then\n return 0\n fi\n\n find \"$worktree_root\" -mindepth 2 -maxdepth 2 -type d 2>/dev/null | while read -r dir; do\n local alias=$(basename \"$(dirname \"$dir\")\")\n local name=$(basename \"$dir\")\n echo \"${alias}:${name}|${dir}\"\n done\n}\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6857,"content_sha256":"11587b7eda2c140572143c845307748e0df2edfbfccb6a73c9c3b113b3a7d6c7"},{"filename":"psm.sh","content":"#!/usr/bin/env bash\n# Project Session Manager (PSM) - Main Script\n# Usage: psm.sh \u003ccommand> [args...]\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# Source library files\nsource \"$SCRIPT_DIR/lib/config.sh\"\nsource \"$SCRIPT_DIR/lib/parse.sh\"\nsource \"$SCRIPT_DIR/lib/worktree.sh\"\nsource \"$SCRIPT_DIR/lib/tmux.sh\"\nsource \"$SCRIPT_DIR/lib/session.sh\"\n\n# Source provider files\nsource \"$SCRIPT_DIR/lib/providers/interface.sh\"\nsource \"$SCRIPT_DIR/lib/providers/github.sh\"\nsource \"$SCRIPT_DIR/lib/providers/jira.sh\"\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nNC='\\033[0m' # No Color\n\n# Logging\nlog_info() { echo -e \"${BLUE}[PSM]${NC} $*\"; }\nlog_success() { echo -e \"${GREEN}[PSM]${NC} $*\"; }\nlog_warn() { echo -e \"${YELLOW}[PSM]${NC} $*\"; }\nlog_error() { echo -e \"${RED}[PSM]${NC} $*\" >&2; }\n\n# Validate a worktree-creation result string against the expected protocol schema.\n# Fails closed on any contamination so a malformed result never becomes a wrong/empty cwd.\n# Usage: psm_validate_worktree_result \u003craw_result> \u003cschema> (schema: pr|branched)\npsm_validate_worktree_result() {\n local raw=\"$1\"\n local schema=\"$2\"\n # Only 'pr' and 'branched' schemas are supported\n if [[ \"$schema\" != \"pr\" && \"$schema\" != \"branched\" ]]; then\n log_error \"Malformed worktree result: unknown schema\"\n return 1\n fi\n # Reject empty result\n if [[ -z \"$raw\" ]]; then\n log_error \"Malformed worktree result: empty result\"\n return 1\n fi\n # Reject embedded newline (prepended OR appended noise -> more than one logical line)\n if [[ \"$raw\" == *

Project Session Manager (PSM) Skill is the compatibility alias for this canonical skill entrypoint. Quick Start (worktree-first): Start with when you want an isolated issue/PR/feature worktree before adding any tmux/session orchestration: See Teleport Command below for details. Automate isolated development environments using git worktrees and tmux sessions with Claude Code. Enables parallel work across multiple tasks, projects, and repositories. Canonical slash command: (alias: ). Commands | Command | Description | Example | |---------|-------------|---------| | | PR review session | | | | I…

\\n'* ]]; then\n log_error \"Malformed worktree result: unexpected multiline output\"\n return 1\n fi\n # Split on '|' WITHOUT dropping trailing empty fields ('read -a' would drop them,\n # which would let a stray trailing delimiter smuggle in an extra field).\n local -a fields=()\n local remainder=\"$raw\"\n while true; do\n fields+=(\"${remainder%%|*}\")\n [[ \"$remainder\" == *\"|\"* ]] || break\n remainder=\"${remainder#*|}\"\n done\n local status=\"${fields[0]}\"\n local count=\"${#fields[@]}\"\n local path=\"${fields[1]:-}\"\n local branch=\"${fields[2]:-}\"\n case \"$status\" in\n created|exists)\n # Required fields must be present and not whitespace-only, or the caller\n # could launch tmux in an empty/unusable working directory.\n case \"$schema\" in\n pr)\n if [[ \"$count\" -ne 2 ]] || [[ \"$path\" =~ ^[[:space:]]*$ ]]; then\n log_error \"Malformed worktree result: invalid $status record\"\n return 1\n fi\n ;;\n branched)\n if [[ \"$count\" -ne 3 ]] || [[ \"$path\" =~ ^[[:space:]]*$ ]] || [[ \"$branch\" =~ ^[[:space:]]*$ ]]; then\n log_error \"Malformed worktree result: invalid $status record\"\n return 1\n fi\n ;;\n esac\n ;;\n error)\n # error|message : exactly 2 fields, message is an opaque diagnostic (not a path)\n if [[ \"$count\" -ne 2 ]] || [[ \"${fields[1]:-}\" =~ ^[[:space:]]*$ ]]; then\n log_error \"Malformed worktree result: invalid error record\"\n return 1\n fi\n ;;\n *)\n log_error \"Malformed worktree result: invalid status\"\n return 1\n ;;\n esac\n return 0\n}\n\n\n# Check dependencies\ncheck_dependencies() {\n local missing=()\n\n if ! command -v git &> /dev/null; then\n missing+=(\"git\")\n fi\n\n if ! command -v jq &> /dev/null; then\n missing+=(\"jq\")\n fi\n\n # Note: gh and jira are checked per-operation, not globally\n # This allows users without gh to still use Jira, and vice versa\n\n if [[ ${#missing[@]} -gt 0 ]]; then\n log_error \"Missing required dependencies: ${missing[*]}\"\n log_info \"Install with:\"\n log_info \" Ubuntu/Debian: sudo apt install git jq\"\n log_info \" macOS: brew install git jq\"\n exit 1\n fi\n\n # tmux is optional but warn if missing\n if ! command -v tmux &> /dev/null; then\n log_warn \"tmux not found. Sessions will be created without tmux.\"\n fi\n}\n\n# Print usage\nusage() {\n cat \u003c\u003c 'EOF'\nProject Session Manager (PSM) - Isolated dev environments\n\nUsage: psm \u003ccommand> [args...]\n\nCommands:\n review \u003cref> Create PR review session\n fix \u003cref> Create issue fix session\n feature \u003cproj> \u003cname> Create feature development session\n list [project] List active sessions\n attach \u003csession> Attach to existing session\n kill \u003csession> Kill and cleanup session\n cleanup [--force] Clean merged PRs and closed issues\n status Show current session info\n\nReference formats:\n omc#123 Project alias + number\n owner/repo#123 Full GitHub reference\n https://... GitHub URL\n #123 Number only (uses current repo)\n\nExamples:\n psm review omc#123\n psm fix Yeachan-Heo/oh-my-claudecode#42\n psm feature omc add-webhooks\n psm list\n psm attach omc:pr-123\n psm kill omc:pr-123\n psm cleanup\nEOF\n}\n\n# Command: review\ncmd_review() {\n local ref=\"$1\"\n local no_claude=\"${2:-false}\"\n local no_tmux=\"${3:-false}\"\n\n log_info \"Parsing reference: $ref\"\n\n # Parse reference\n local parsed\n parsed=$(psm_parse_ref \"$ref\")\n if [[ $? -ne 0 ]] || [[ \"$parsed\" == error* ]]; then\n log_error \"Failed to parse reference: $ref\"\n return 1\n fi\n\n IFS='|' read -r type alias repo pr_number local_path base provider provider_ref \u003c\u003c\u003c \"$parsed\"\n\n # Provider guard: Jira doesn't have PRs\n if [[ \"$provider\" == \"jira\" ]]; then\n log_error \"Jira issues cannot be 'reviewed' - Jira has no PR concept.\"\n log_info \"Use 'psm fix $ref' to work on a Jira issue instead.\"\n log_info \"Jira integration supports: fix, feature\"\n return 1\n fi\n\n # Check GitHub CLI availability\n if ! provider_github_available; then\n log_error \"GitHub CLI (gh) not found. Install: brew install gh\"\n return 1\n fi\n\n if [[ -z \"$repo\" ]]; then\n log_error \"Could not determine repository\"\n return 1\n fi\n\n log_info \"Fetching PR #${pr_number} from ${repo}...\"\n\n # Fetch PR info\n local pr_info\n pr_info=$(provider_call \"github\" fetch_pr \"$pr_number\" \"$repo\") || {\n log_error \"Failed to fetch PR #${pr_number}. Check if the PR exists and you have access.\"\n return 1\n }\n\n local pr_title=$(echo \"$pr_info\" | jq -r '.title')\n local pr_author=$(echo \"$pr_info\" | jq -r '.author.login')\n local head_branch=$(echo \"$pr_info\" | jq -r '.headRefName')\n local base_branch=$(echo \"$pr_info\" | jq -r '.baseRefName')\n local pr_url=$(echo \"$pr_info\" | jq -r '.url')\n local pr_body=$(echo \"$pr_info\" | jq -r '.body // \"\"')\n local changed_files=$(echo \"$pr_info\" | jq -r '.files[]?.path // empty' 2>/dev/null | head -50 || true)\n\n log_info \"PR: #${pr_number} - ${pr_title}\"\n log_info \"Author: @${pr_author}\"\n log_info \"Branch: ${head_branch} -> ${base_branch}\"\n\n # Determine alias if not set\n if [[ -z \"$alias\" ]]; then\n alias=$(echo \"$repo\" | tr '/' '-')\n fi\n\n # Determine local path\n if [[ -z \"$local_path\" || ! -d \"$local_path\" ]]; then\n # Clone if needed\n local_path=\"${HOME}/Workspace/$(basename \"$repo\")\"\n if [[ ! -d \"$local_path\" ]]; then\n log_info \"Cloning repository to $local_path...\"\n local clone_url\n clone_url=$(provider_call \"github\" clone_url \"$repo\")\n git clone \"$clone_url\" \"$local_path\" || {\n log_error \"Failed to clone repository\"\n return 1\n }\n fi\n fi\n\n # Create worktree\n log_info \"Creating worktree...\"\n local worktree_result worktree_rc=0\n worktree_result=$(psm_create_pr_worktree \"$local_path\" \"$alias\" \"$pr_number\" \"$head_branch\") || worktree_rc=$?\n\n local worktree_status\n local worktree_path\n IFS='|' read -r worktree_status worktree_path \u003c\u003c\u003c \"$worktree_result\"\n if ! psm_validate_worktree_result \"$worktree_result\" pr; then\n return 1\n fi\n\n\n if [[ \"$worktree_status\" == \"exists\" ]]; then\n log_warn \"Worktree already exists at $worktree_path\"\n log_info \"Use 'psm attach ${alias}:pr-${pr_number}' to attach\"\n return 0\n elif [[ \"$worktree_status\" == \"error\" ]]; then\n log_error \"Failed to create worktree: $worktree_path\"\n return 1\n fi\n\n log_success \"Worktree created at $worktree_path\"\n\n # Render PR review context file into the worktree so Claude starts with full task context\n local context_rel=\".psm/review.md\"\n local context_file=\"${worktree_path}/${context_rel}\"\n mkdir -p \"$(dirname \"$context_file\")\"\n if ! psm_render_template \"$SCRIPT_DIR/templates/pr-review.md\" \\\n \"PR_NUMBER=${pr_number}\" \\\n \"PR_TITLE=${pr_title}\" \\\n \"PR_AUTHOR=${pr_author}\" \\\n \"PR_URL=${pr_url}\" \\\n \"HEAD_BRANCH=${head_branch}\" \\\n \"BASE_BRANCH=${base_branch}\" \\\n \"PR_BODY=${pr_body}\" \\\n \"CHANGED_FILES=${changed_files}\" \\\n > \"$context_file\"; then\n log_warn \"Failed to render review template; Claude will start without task context\"\n context_rel=\"\"\n fi\n\n # Create tmux session\n local session_id=\"${alias}:pr-${pr_number}\"\n local session_name=$(psm_tmux_name_from_id \"$session_id\")\n\n if [[ \"$no_tmux\" != \"true\" ]]; then\n log_info \"Creating tmux session...\"\n local tmux_result\n tmux_result=$(psm_create_tmux_session \"$session_name\" \"$worktree_path\")\n\n local tmux_status\n IFS='|' read -r tmux_status _ \u003c\u003c\u003c \"$tmux_result\"\n\n if [[ \"$tmux_status\" == \"error\" ]]; then\n log_warn \"Could not create tmux session. Continuing without tmux.\"\n elif [[ \"$tmux_status\" == \"exists\" ]]; then\n log_warn \"Tmux session already exists\"\n else\n log_success \"Tmux session created: $session_name\"\n\n # Launch Claude Code with review context so it starts on the PR task\n if [[ \"$no_claude\" != \"true\" ]]; then\n log_info \"Launching Claude Code...\"\n psm_launch_claude \"$session_name\" \"$context_rel\"\n fi\n fi\n fi\n\n # Create session metadata\n local metadata\n metadata=$(jq -n \\\n --argjson pr_number \"$pr_number\" \\\n --arg pr_title \"$pr_title\" \\\n --arg pr_author \"$pr_author\" \\\n --arg pr_url \"$pr_url\" \\\n '{pr_number: $pr_number, pr_title: $pr_title, pr_author: $pr_author, pr_url: $pr_url}')\n\n # Add to registry\n psm_add_session \"$session_id\" \"review\" \"$alias\" \"pr-${pr_number}\" \"$head_branch\" \"$base_branch\" \"$session_name\" \"$worktree_path\" \"$local_path\" \"$metadata\" \"github\" \"${repo}#${pr_number}\"\n\n # Output summary\n echo \"\"\n log_success \"Session ready!\"\n echo \"\"\n echo \" ID: $session_id\"\n echo \" Type: review\"\n echo \" PR: #${pr_number} - ${pr_title}\"\n echo \" Worktree: $worktree_path\"\n echo \" Tmux: $session_name\"\n echo \"\"\n echo \"Commands:\"\n echo \" Attach: tmux attach -t $session_name\"\n echo \" Kill: psm kill $session_id\"\n echo \" Cleanup: psm cleanup\"\n echo \"\"\n}\n\n# Command: fix\ncmd_fix() {\n local ref=\"$1\"\n local no_claude=\"${2:-false}\"\n\n log_info \"Parsing reference: $ref\"\n\n local parsed\n parsed=$(psm_parse_ref \"$ref\")\n if [[ $? -ne 0 ]] || [[ \"$parsed\" == error* ]]; then\n log_error \"Failed to parse reference: $ref\"\n return 1\n fi\n\n IFS='|' read -r type alias repo issue_number local_path base provider provider_ref \u003c\u003c\u003c \"$parsed\"\n\n # Check provider CLI availability\n if [[ \"$provider\" == \"jira\" ]]; then\n if ! provider_jira_available; then\n log_error \"Jira CLI not found. Install: brew install ankitpokhrel/jira-cli/jira-cli\"\n return 1\n fi\n else\n if ! provider_github_available; then\n log_error \"GitHub CLI (gh) not found. Install: brew install gh\"\n return 1\n fi\n fi\n\n if [[ -z \"$repo\" && \"$provider\" != \"jira\" ]]; then\n log_error \"Could not determine repository\"\n return 1\n fi\n\n log_info \"Fetching issue #${issue_number}...\"\n\n # Fetch issue info\n local issue_info\n local issue_body=\"\" issue_labels=\"\"\n if [[ \"$provider\" == \"jira\" ]]; then\n issue_info=$(provider_call \"jira\" fetch_issue \"$provider_ref\") || {\n log_error \"Failed to fetch Jira issue ${provider_ref}\"\n return 1\n }\n local issue_title=$(echo \"$issue_info\" | jq -r '.fields.summary')\n local issue_url=$(echo \"$issue_info\" | jq -r '.self // empty')\n issue_body=$(echo \"$issue_info\" | jq -r '.fields.description // \"\"')\n else\n issue_info=$(provider_call \"github\" fetch_issue \"$issue_number\" \"$repo\") || {\n log_error \"Failed to fetch issue #${issue_number}\"\n return 1\n }\n local issue_title=$(echo \"$issue_info\" | jq -r '.title')\n local issue_url=$(echo \"$issue_info\" | jq -r '.url')\n issue_body=$(echo \"$issue_info\" | jq -r '.body // \"\"')\n issue_labels=$(echo \"$issue_info\" | jq -r '[.labels[].name] | join(\", \")' 2>/dev/null || true)\n fi\n local slug=$(psm_slugify \"$issue_title\" 20)\n\n log_info \"Issue: #${issue_number} - ${issue_title}\"\n\n # Determine alias\n if [[ -z \"$alias\" ]]; then\n alias=$(echo \"$repo\" | tr '/' '-')\n fi\n\n # Determine local path\n if [[ -z \"$local_path\" || ! -d \"$local_path\" ]]; then\n local_path=\"${HOME}/Workspace/$(basename \"${repo:-$alias}\")\"\n if [[ ! -d \"$local_path\" ]]; then\n log_info \"Cloning repository...\"\n local clone_url\n if [[ \"$provider\" == \"jira\" ]]; then\n clone_url=$(provider_call \"jira\" clone_url \"$alias\") || {\n log_error \"Failed to get clone URL for '$alias'. Configure 'repo' or 'clone_url' in projects.json\"\n return 1\n }\n else\n clone_url=$(provider_call \"github\" clone_url \"$repo\")\n fi\n git clone \"$clone_url\" \"$local_path\" || return 1\n fi\n fi\n\n # Create worktree\n log_info \"Creating worktree and branch...\"\n local worktree_result worktree_rc=0\n worktree_result=$(psm_create_issue_worktree \"$local_path\" \"$alias\" \"$issue_number\" \"$slug\" \"$base\") || worktree_rc=$?\n\n local worktree_status worktree_path branch_name\n IFS='|' read -r worktree_status worktree_path branch_name \u003c\u003c\u003c \"$worktree_result\"\n if ! psm_validate_worktree_result \"$worktree_result\" branched; then\n return 1\n fi\n\n\n if [[ \"$worktree_status\" == \"exists\" ]]; then\n log_warn \"Worktree already exists at $worktree_path\"\n return 0\n elif [[ \"$worktree_status\" == \"error\" ]]; then\n log_error \"Failed to create worktree: $worktree_path\"\n return 1\n fi\n\n log_success \"Worktree created at $worktree_path\"\n log_info \"Branch: $branch_name\"\n\n # Render issue fix context file into the worktree so Claude starts with full task context\n local fix_context_rel=\".psm/fix.md\"\n local fix_context_file=\"${worktree_path}/${fix_context_rel}\"\n mkdir -p \"$(dirname \"$fix_context_file\")\"\n if ! psm_render_template \"$SCRIPT_DIR/templates/issue-fix.md\" \\\n \"ISSUE_NUMBER=${issue_number}\" \\\n \"ISSUE_TITLE=${issue_title}\" \\\n \"ISSUE_URL=${issue_url}\" \\\n \"ISSUE_LABELS=${issue_labels}\" \\\n \"ISSUE_BODY=${issue_body}\" \\\n \"BRANCH_NAME=${branch_name}\" \\\n > \"$fix_context_file\"; then\n log_warn \"Failed to render issue fix template; Claude will start without task context\"\n fix_context_rel=\"\"\n fi\n\n # Create tmux session\n local session_id=\"${alias}:issue-${issue_number}\"\n local session_name=$(psm_tmux_name_from_id \"$session_id\")\n\n log_info \"Creating tmux session...\"\n psm_create_tmux_session \"$session_name\" \"$worktree_path\"\n\n if [[ \"$no_claude\" != \"true\" ]]; then\n psm_launch_claude \"$session_name\" \"$fix_context_rel\"\n fi\n\n # Create metadata\n local metadata\n metadata=$(jq -n \\\n --argjson issue_number \"$issue_number\" \\\n --arg issue_title \"$issue_title\" \\\n --arg issue_url \"$issue_url\" \\\n '{issue_number: $issue_number, issue_title: $issue_title, issue_url: $issue_url}')\n\n psm_add_session \"$session_id\" \"fix\" \"$alias\" \"issue-${issue_number}\" \"$branch_name\" \"$base\" \"$session_name\" \"$worktree_path\" \"$local_path\" \"$metadata\" \"$provider\" \"$provider_ref\"\n\n echo \"\"\n log_success \"Session ready!\"\n echo \"\"\n echo \" ID: $session_id\"\n echo \" Type: fix\"\n echo \" Issue: #${issue_number} - ${issue_title}\"\n echo \" Branch: $branch_name\"\n echo \" Worktree: $worktree_path\"\n echo \" Tmux: $session_name\"\n echo \"\"\n}\n\n# Command: feature\ncmd_feature() {\n local project=\"$1\"\n local feature_name=\"$2\"\n\n log_info \"Creating feature session for: $feature_name\"\n\n # Resolve project\n local project_info\n project_info=$(psm_get_project \"$project\")\n if [[ $? -ne 0 ]]; then\n log_error \"Unknown project: $project\"\n return 1\n fi\n\n IFS='|' read -r repo local_path base \u003c\u003c\u003c \"$project_info\"\n\n if [[ ! -d \"$local_path\" ]]; then\n log_error \"Local path not found: $local_path\"\n return 1\n fi\n\n # Create worktree\n log_info \"Creating worktree and branch...\"\n local worktree_result worktree_rc=0\n worktree_result=$(psm_create_feature_worktree \"$local_path\" \"$project\" \"$feature_name\" \"$base\") || worktree_rc=$?\n\n local worktree_status worktree_path branch_name\n IFS='|' read -r worktree_status worktree_path branch_name \u003c\u003c\u003c \"$worktree_result\"\n if ! psm_validate_worktree_result \"$worktree_result\" branched; then\n return 1\n fi\n\n if [[ \"$worktree_status\" == \"exists\" ]]; then\n log_warn \"Worktree already exists at $worktree_path\"\n return 0\n elif [[ \"$worktree_status\" == \"error\" ]]; then\n log_error \"Failed to create worktree\"\n return 1\n fi\n\n log_success \"Worktree created at $worktree_path\"\n\n local safe_name=$(psm_sanitize \"$feature_name\")\n local session_id=\"${project}:feat-${safe_name}\"\n local session_name=$(psm_tmux_name_from_id \"$session_id\")\n\n psm_create_tmux_session \"$session_name\" \"$worktree_path\"\n local feature_prompt=\"Implement feature \\\"${feature_name}\\\" for project ${project}. Working branch: ${branch_name}. Build the feature, add tests, and open a PR when ready: gh pr create --title \\\"feat: ${feature_name}\\\"\"\n psm_launch_claude \"$session_name\" \"$feature_prompt\"\n\n psm_add_session \"$session_id\" \"feature\" \"$project\" \"feat-${safe_name}\" \"$branch_name\" \"$base\" \"$session_name\" \"$worktree_path\" \"$local_path\" \"{}\"\n\n echo \"\"\n log_success \"Session ready!\"\n echo \"\"\n echo \" ID: $session_id\"\n echo \" Type: feature\"\n echo \" Branch: $branch_name\"\n echo \" Worktree: $worktree_path\"\n echo \" Tmux: $session_name\"\n echo \"\"\n}\n\n# Command: list\ncmd_list() {\n local project=\"${1:-}\"\n\n echo \"\"\n echo \"Active PSM Sessions:\"\n echo \"\"\n printf \"%-25s | %-8s | %-10s | %s\\n\" \"ID\" \"Type\" \"State\" \"Worktree\"\n printf \"%-25s-+-%-8s-+-%-10s-+-%s\\n\" \"-------------------------\" \"--------\" \"----------\" \"----------------------------------------\"\n\n psm_list_sessions \"$project\" | while IFS='|' read -r id type state worktree; do\n # Check if tmux session exists\n local tmux_state=\"detached\"\n if psm_tmux_session_exists \"$(psm_tmux_name_from_id \"$id\")\"; then\n tmux_state=\"$state\"\n else\n tmux_state=\"no-tmux\"\n fi\n\n printf \"%-25s | %-8s | %-10s | %s\\n\" \"$id\" \"$type\" \"$tmux_state\" \"$worktree\"\n done\n\n echo \"\"\n}\n\n# Command: attach\ncmd_attach() {\n local session_id=\"$1\"\n local session_name=$(psm_tmux_name_from_id \"$session_id\")\n\n if ! psm_tmux_session_exists \"$session_name\"; then\n log_error \"Session not found: $session_name\"\n log_info \"Use 'psm list' to see available sessions\"\n return 1\n fi\n\n echo \"Attaching to $session_name...\"\n echo \"Run: tmux attach -t $session_name\"\n}\n\n# Command: kill\ncmd_kill() {\n local session_id=\"$1\"\n\n log_info \"Killing session: $session_id\"\n\n # Get session info\n local session_json\n session_json=$(psm_get_session \"$session_id\")\n if [[ -z \"$session_json\" ]]; then\n log_error \"Session not found in registry: $session_id\"\n return 1\n fi\n\n local tmux_name=$(echo \"$session_json\" | jq -r '.tmux')\n local worktree_path=$(echo \"$session_json\" | jq -r '.worktree')\n local source_repo=$(echo \"$session_json\" | jq -r '.source_repo')\n\n # Kill tmux\n psm_kill_tmux_session \"$tmux_name\"\n log_info \"Killed tmux session: $tmux_name\"\n\n # Remove worktree\n psm_remove_worktree \"$source_repo\" \"$worktree_path\"\n log_info \"Removed worktree: $worktree_path\"\n\n # Remove from registry\n psm_remove_session \"$session_id\"\n log_info \"Removed from registry\"\n\n log_success \"Session killed: $session_id\"\n}\n\n# Command: cleanup\ncmd_cleanup() {\n local force=\"${1:-false}\"\n\n log_info \"Starting cleanup...\"\n\n local cleaned=0\n\n # Check PR sessions (GitHub only)\n while IFS='|' read -r id pr_number project; do\n if [[ -z \"$id\" ]]; then continue; fi\n\n local session_json=$(psm_get_session \"$id\")\n local provider=$(echo \"$session_json\" | jq -r '.provider // \"github\"')\n\n # Only GitHub has PRs\n if [[ \"$provider\" != \"github\" ]]; then continue; fi\n\n local repo=$(psm_get_project \"$project\" | cut -d'|' -f1)\n\n if [[ -n \"$repo\" && -n \"$pr_number\" ]]; then\n if provider_github_available && provider_call \"github\" pr_merged \"$pr_number\" \"$repo\"; then\n log_info \"PR #${pr_number} is merged - cleaning up $id\"\n cmd_kill \"$id\"\n ((cleaned++))\n fi\n fi\n done \u003c \u003c(psm_get_review_sessions)\n\n # Check issue sessions (GitHub and Jira)\n while IFS='|' read -r id issue_number project; do\n if [[ -z \"$id\" ]]; then continue; fi\n\n local session_json=$(psm_get_session \"$id\")\n local provider=$(echo \"$session_json\" | jq -r '.provider // \"github\"')\n local provider_ref=$(echo \"$session_json\" | jq -r '.provider_ref // empty')\n\n if [[ \"$provider\" == \"jira\" ]]; then\n # Jira cleanup\n if provider_jira_available && [[ -n \"$provider_ref\" ]]; then\n if provider_call \"jira\" issue_closed \"$provider_ref\"; then\n log_info \"Jira issue ${provider_ref} is done - cleaning up $id\"\n cmd_kill \"$id\"\n ((cleaned++))\n fi\n fi\n else\n # GitHub cleanup\n local repo=$(psm_get_project \"$project\" | cut -d'|' -f1)\n if provider_github_available && [[ -n \"$repo\" && -n \"$issue_number\" ]]; then\n if provider_call \"github\" issue_closed \"$issue_number\" \"$repo\"; then\n log_info \"Issue #${issue_number} is closed - cleaning up $id\"\n cmd_kill \"$id\"\n ((cleaned++))\n fi\n fi\n fi\n done \u003c \u003c(psm_get_fix_sessions)\n\n if [[ $cleaned -eq 0 ]]; then\n log_success \"Cleanup complete - no sessions to clean\"\n else\n log_success \"Cleanup complete - removed $cleaned session(s)\"\n fi\n}\n\n# Command: status\ncmd_status() {\n # Try to detect current session\n local current_session=$(psm_current_tmux_session)\n\n if [[ -n \"$current_session\" && \"$current_session\" == psm_* ]]; then\n local session_id=$(psm_get_session_id_for_tmux \"$current_session\")\n local session_json=$(psm_get_session \"$session_id\")\n\n if [[ -n \"$session_json\" ]]; then\n echo \"\"\n echo \"Current Session: $session_id\"\n echo \"\"\n echo \" Type: $(echo \"$session_json\" | jq -r '.type')\"\n echo \" Branch: $(echo \"$session_json\" | jq -r '.branch')\"\n echo \" Base: $(echo \"$session_json\" | jq -r '.base')\"\n echo \" Worktree: $(echo \"$session_json\" | jq -r '.worktree')\"\n echo \" Created: $(echo \"$session_json\" | jq -r '.created_at')\"\n echo \"\"\n return 0\n fi\n fi\n\n # Check if we're in a worktree\n local cwd=$(pwd)\n local worktree_root=$(psm_get_worktree_root)\n\n if [[ \"$cwd\" == \"$worktree_root\"* ]]; then\n local meta_file=\"${cwd}/.psm-session.json\"\n if [[ -f \"$meta_file\" ]]; then\n cat \"$meta_file\" | jq .\n return 0\n fi\n fi\n\n log_info \"Not in a PSM session\"\n log_info \"Use 'psm list' to see available sessions\"\n}\n\n# Main entry point\nmain() {\n if [[ $# -eq 0 ]]; then\n usage\n exit 0\n fi\n\n # Check dependencies first\n check_dependencies\n\n # Initialize PSM\n psm_init\n\n local cmd=\"$1\"\n shift\n\n case \"$cmd\" in\n review|r|pr)\n if [[ $# -lt 1 ]]; then\n log_error \"Usage: psm review \u003cref>\"\n exit 1\n fi\n cmd_review \"$@\"\n ;;\n fix|issue|i)\n if [[ $# -lt 1 ]]; then\n log_error \"Usage: psm fix \u003cref>\"\n exit 1\n fi\n cmd_fix \"$@\"\n ;;\n feature|feat|f)\n if [[ $# -lt 2 ]]; then\n log_error \"Usage: psm feature \u003cproject> \u003cname>\"\n exit 1\n fi\n cmd_feature \"$@\"\n ;;\n list|ls|l)\n cmd_list \"$@\"\n ;;\n attach|a)\n if [[ $# -lt 1 ]]; then\n log_error \"Usage: psm attach \u003csession>\"\n exit 1\n fi\n cmd_attach \"$@\"\n ;;\n kill|k|rm)\n if [[ $# -lt 1 ]]; then\n log_error \"Usage: psm kill \u003csession>\"\n exit 1\n fi\n cmd_kill \"$@\"\n ;;\n cleanup|gc|clean)\n cmd_cleanup \"$@\"\n ;;\n status|st)\n cmd_status\n ;;\n help|-h|--help)\n usage\n ;;\n *)\n log_error \"Unknown command: $cmd\"\n usage\n exit 1\n ;;\n esac\n}\n\n# Run if executed directly\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n main \"$@\"\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":26724,"content_sha256":"db732b9c7a29dcd4960c9f97c19fd756a055d283ec61fcd515f4b65213429a8f"},{"filename":"templates/feature.md","content":"# Feature Development Context\n\nYou are developing feature: **{{FEATURE_NAME}}**\n\n## Details\n\n- **Branch**: `{{BRANCH_NAME}}`\n- **Base**: `{{BASE_BRANCH}}`\n- **Project**: {{PROJECT}}\n\n## Feature Scope\n\n{{FEATURE_DESCRIPTION}}\n\n## Development Approach\n\n1. **Plan**\n - Define requirements\n - Break into subtasks\n - Identify dependencies\n\n2. **Implement**\n - Follow project patterns\n - Write clean, testable code\n - Commit incrementally\n\n3. **Test**\n - Unit tests for new code\n - Integration tests if needed\n - Manual testing\n\n4. **Document**\n - Update relevant docs\n - Add code comments where needed\n - Update CHANGELOG if applicable\n\n## Commands\n\n```bash\n# Run tests\nnpm test # or appropriate test command\n\n# Check build\nnpm run build # or appropriate build command\n\n# Create PR when ready\ngh pr create --title \"Feature: {{FEATURE_NAME}}\" --body \"## Summary\\n\\n\u003cdescription>\\n\\n## Changes\\n\\n- \u003cchange 1>\\n- \u003cchange 2>\"\n```\n\n## Feature Checklist\n\n- [ ] Requirements understood\n- [ ] Implementation complete\n- [ ] Tests written and passing\n- [ ] Documentation updated\n- [ ] Ready for PR\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1110,"content_sha256":"789e359113455f58a17247a95ed4f36676ecf39d435668c9e9bb00871d2e66a9"},{"filename":"templates/issue-fix.md","content":"# Issue Fix Context\n\nYou are fixing Issue #{{ISSUE_NUMBER}}: **{{ISSUE_TITLE}}**\n\n## Issue Details\n\n- **URL**: {{ISSUE_URL}}\n- **Labels**: {{ISSUE_LABELS}}\n- **Branch**: `{{BRANCH_NAME}}`\n\n## Description\n\n{{ISSUE_BODY}}\n\n## Approach\n\n1. **Understand the Issue**\n - Reproduce the problem if applicable\n - Identify root cause\n - Consider edge cases\n\n2. **Plan the Fix**\n - Minimal changes to fix the issue\n - Don't introduce regressions\n - Consider backwards compatibility\n\n3. **Implement**\n - Write the fix\n - Add/update tests\n - Update documentation if needed\n\n4. **Verify**\n - Run existing tests\n - Test the specific fix\n - Check for regressions\n\n## Commands\n\n```bash\n# Run tests\nnpm test # or appropriate test command\n\n# Check build\nnpm run build # or appropriate build command\n\n# Create PR when done\ngh pr create --title \"Fix #{{ISSUE_NUMBER}}: \u003cdescription>\" --body \"Fixes #{{ISSUE_NUMBER}}\"\n```\n\n## Fix Checklist\n\n- [ ] Root cause identified\n- [ ] Fix implemented\n- [ ] Tests added/updated\n- [ ] All tests pass\n- [ ] No regressions introduced\n- [ ] Ready for PR\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1093,"content_sha256":"149f246033caaa2247681c2f740d165806166cb52acc0a279b638bc5d0fb201c"},{"filename":"templates/pr-review.md","content":"# PR Review Context\n\nYou are reviewing PR #{{PR_NUMBER}}: **{{PR_TITLE}}**\n\n## PR Details\n\n- **Author**: @{{PR_AUTHOR}}\n- **Branch**: `{{HEAD_BRANCH}}` → `{{BASE_BRANCH}}`\n- **URL**: {{PR_URL}}\n\n## Description\n\n{{PR_BODY}}\n\n## Changed Files\n\n{{CHANGED_FILES}}\n\n## Review Focus\n\n1. **Code Quality**\n - Follow existing patterns and conventions\n - Clean, readable, maintainable code\n - Appropriate abstractions\n\n2. **Correctness**\n - Does it do what it claims?\n - Edge cases handled?\n - Error handling appropriate?\n\n3. **Security**\n - Input validation\n - No hardcoded secrets\n - Safe dependencies\n\n4. **Testing**\n - Adequate test coverage\n - Tests are meaningful\n - Edge cases tested\n\n5. **Documentation**\n - Code is self-documenting\n - Complex logic explained\n - API changes documented\n\n## Commands\n\n```bash\n# View diff\ngit diff {{BASE_BRANCH}}...HEAD\n\n# Run the narrowest relevant tests first\n# If this clean review worktree has a symlinked node_modules from the source repo,\n# focused vitest commands should work without a fresh install.\nnpm run test:run -- \u003cchanged-test-paths> # preferred focused verification\nnpm test # or appropriate full test command if focused coverage is insufficient\n\n# Check build\nnpm run build # or appropriate build command\n```\n\n## Review Checklist\n\n- [ ] Code follows project style\n- [ ] No obvious bugs or logic errors\n- [ ] Security concerns addressed\n- [ ] Tests pass and cover changes\n- [ ] Documentation updated if needed\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1491,"content_sha256":"de04ff51ce7eb37b6ff51cc7b19c5d0a2798cfbcc6bc28fb28959ac0723fe2e5"},{"filename":"templates/projects.json","content":"{\n \"aliases\": {\n \"omc\": {\n \"repo\": \"Yeachan-Heo/oh-my-claudecode\",\n \"local\": \"~/Workspace/oh-my-claudecode\",\n \"default_base\": \"main\"\n },\n \"cc\": {\n \"repo\": \"anthropics/claude-code\",\n \"local\": \"~/Workspace/claude-code\",\n \"default_base\": \"main\"\n }\n },\n \"defaults\": {\n \"worktree_root\": \"~/.psm/worktrees\",\n \"cleanup_after_days\": 14,\n \"auto_cleanup_merged\": true\n }\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":418,"content_sha256":"9d12e615b32e1833f70c788f4c2fb43dbe24fceb7b9dee9c499ddafc74bd08aa"},{"filename":"tests/test-jira-provider.sh","content":"#!/usr/bin/env bash\n# Regression tests for PSM Jira provider (issue #3526)\n# Root cause: provider_jira_fetch_issue / provider_jira_issue_closed passed\n# `--output json` to jira-cli, but jira-cli (v1.7) has no `--output` flag.\n# `2>/dev/null` hid the \"unknown flag\" error, so every fetch returned empty\n# with exit 1 and `psm fix \u003calias>#\u003cn>` aborted. The correct flag is `--raw`.\n#\n# Usage: bash skills/project-session-manager/tests/test-jira-provider.sh\n\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPSM_LIB_DIR=\"${SCRIPT_DIR}/../lib\"\n\n# ── Test counters ─────────────────────────────────────────────────────────────\n\nPASS=0\nFAIL=0\n\npass() { echo \"PASS: $1\"; (( PASS++ )) || true; }\nfail() { echo \"FAIL: $1\"; echo \" $2\"; (( FAIL++ )) || true; }\n\nassert_contains() {\n local desc=\"$1\" needle=\"$2\" haystack=\"$3\"\n if printf '%s' \"$haystack\" | grep -qF -- \"$needle\"; then\n pass \"$desc\"\n else\n fail \"$desc\" \"expected to contain: '$needle' | actual: '$haystack'\"\n fi\n}\n\nassert_not_contains() {\n local desc=\"$1\" needle=\"$2\" haystack=\"$3\"\n if ! printf '%s' \"$haystack\" | grep -qF -- \"$needle\"; then\n pass \"$desc\"\n else\n fail \"$desc\" \"expected NOT to contain: '$needle' | actual: '$haystack'\"\n fi\n}\n\n# ── Setup ─────────────────────────────────────────────────────────────────────\n\nsource \"${PSM_LIB_DIR}/providers/jira.sh\"\n\n# Temp file for capturing argv across command-substitution subshells.\n# provider functions run inside $(...), so a plain variable set by the stub\n# would be lost with the subshell — write to a file the parent can read.\nJIRA_ARGV_FILE=\"$(mktemp)\"\ncleanup() { rm -f \"$JIRA_ARGV_FILE\"; }\ntrap cleanup EXIT\n\n# Fixture body emitted by the `jira` stub on stdout (raw Jira API JSON shape).\nJIRA_STUB_STDOUT=\"\"\n\n# Stub the real `jira` binary. Records argv to a file and emits the fixture body.\n# Mirrors real behavior: any unknown flag would go to stderr with exit 2, so a\n# regression to `--output json` would produce empty stdout here and fail the\n# argv assertions below.\njira() {\n printf '%s' \"$*\" > \"$JIRA_ARGV_FILE\"\n if [[ \"$*\" == *\"--output\"* ]]; then\n # Simulate jira-cli rejecting the non-existent flag.\n echo \"Error: unknown flag: --output\" >&2\n return 2\n fi\n printf '%s' \"$JIRA_STUB_STDOUT\"\n}\n\n# Read back the argv captured by the most recent `jira` stub call.\ncaptured_argv() { cat \"$JIRA_ARGV_FILE\"; }\n\n# ── provider_jira_fetch_issue argv ────────────────────────────────────────────\n\necho \"\"\necho \"=== provider_jira_fetch_issue argv ===\"\n\nJIRA_STUB_STDOUT='{\"fields\":{\"summary\":\"Example\"}}'\n: > \"$JIRA_ARGV_FILE\"\nfetch_out=$(provider_jira_fetch_issue \"PL-3966\")\nfetch_argv=\"$(captured_argv)\"\n\n# 1. Uses --raw\nassert_contains \"fetch_issue passes --raw\" \"--raw\" \"$fetch_argv\"\n# 2. Does NOT pass the non-existent --output flag\nassert_not_contains \"fetch_issue omits --output\" \"--output\" \"$fetch_argv\"\n# 3. Does NOT pass a bare json subcommand arg\nassert_not_contains \"fetch_issue omits json arg\" \"json\" \"$fetch_argv\"\n# 4. Targets the issue view subcommand with the key\nassert_contains \"fetch_issue calls issue view for key\" \"issue view PL-3966\" \"$fetch_argv\"\n# 5. Returns the raw JSON body on stdout\nassert_contains \"fetch_issue returns raw JSON body\" '\"summary\":\"Example\"' \"$fetch_out\"\n\n# ── provider_jira_issue_closed argv + status handling ─────────────────────────\n\necho \"\"\necho \"=== provider_jira_issue_closed argv + status handling ===\"\n\n# statusCategory shape mirrors real `jira issue view \u003ckey> --raw` output.\nJIRA_STUB_STDOUT='{\"fields\":{\"summary\":\"Example\",\"status\":{\"statusCategory\":{\"key\":\"done\"}}}}'\n: > \"$JIRA_ARGV_FILE\"\nif provider_jira_issue_closed \"PL-3966\"; then\n closed_done_rc=0\nelse\n closed_done_rc=$?\nfi\nclosed_argv=\"$(captured_argv)\"\n\n# 6. Uses --raw\nassert_contains \"issue_closed passes --raw\" \"--raw\" \"$closed_argv\"\n# 7. Does NOT pass --output\nassert_not_contains \"issue_closed omits --output\" \"--output\" \"$closed_argv\"\n\n# 8. done -> closed (exit 0)\nif [[ \"$closed_done_rc\" -eq 0 ]]; then\n pass \"issue_closed returns 0 for statusCategory 'done'\"\nelse\n fail \"issue_closed returns 0 for statusCategory 'done'\" \"exit=$closed_done_rc\"\nfi\n\n# 9. indeterminate -> not closed (non-zero) — the exact value from the issue report\nJIRA_STUB_STDOUT='{\"fields\":{\"summary\":\"Example\",\"status\":{\"statusCategory\":{\"key\":\"indeterminate\"}}}}'\nif provider_jira_issue_closed \"PL-3966\"; then\n closed_indeterminate_rc=0\nelse\n closed_indeterminate_rc=$?\nfi\nif [[ \"$closed_indeterminate_rc\" -ne 0 ]]; then\n pass \"issue_closed returns non-zero for statusCategory 'indeterminate'\"\nelse\n fail \"issue_closed returns non-zero for statusCategory 'indeterminate'\" \"exit=$closed_indeterminate_rc\"\nfi\n\n# 10. new -> not closed (non-zero)\nJIRA_STUB_STDOUT='{\"fields\":{\"status\":{\"statusCategory\":{\"key\":\"new\"}}}}'\nif provider_jira_issue_closed \"PL-3966\"; then\n closed_new_rc=0\nelse\n closed_new_rc=$?\nfi\nif [[ \"$closed_new_rc\" -ne 0 ]]; then\n pass \"issue_closed returns non-zero for statusCategory 'new'\"\nelse\n fail \"issue_closed returns non-zero for statusCategory 'new'\" \"exit=$closed_new_rc\"\nfi\n\n# ── Regression guard: --output json would break both paths ────────────────────\n\necho \"\"\necho \"=== regression guard ===\"\n\n# If the code regressed to `--output json`, the stub returns empty with exit 2,\n# so fetch would be empty and issue_closed could never see 'done'.\nassert_not_contains \"no --output regression in fetch argv\" \"--output\" \"$fetch_argv\"\nassert_not_contains \"no --output regression in issue_closed argv\" \"--output\" \"$closed_argv\"\n\n# ── Summary ──────────────────────────────────────────────────────────────────\n\necho \"\"\necho \"Results: ${PASS} passed, ${FAIL} failed\"\n[[ $FAIL -eq 0 ]] && exit 0 || exit 1\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":6381,"content_sha256":"35b9f81691bea1e9f367452319e394aed589ac531da99321309e055820363cb2"},{"filename":"tests/test-psm-prompt-injection.sh","content":"#!/usr/bin/env bash\n# Regression tests for PSM prompt injection (issue #2506)\n# Root cause: psm_launch_claude only sent \"claude Enter\" with no task context injected.\n# Fix: psm_render_template + psm_inject_prompt + context file written to worktree.\n#\n# Usage: bash skills/project-session-manager/tests/test-psm-prompt-injection.sh\n\nset -uo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nPSM_LIB_DIR=\"${SCRIPT_DIR}/../lib\"\nTEMPLATES_DIR=\"${SCRIPT_DIR}/../templates\"\n\n# ── Test counters ─────────────────────────────────────────────────────────────\n\nPASS=0\nFAIL=0\n\npass() { echo \"PASS: $1\"; (( PASS++ )) || true; }\nfail() { echo \"FAIL: $1\"; echo \" $2\"; (( FAIL++ )) || true; }\n\nassert_equals() {\n local desc=\"$1\" expected=\"$2\" actual=\"$3\"\n if [[ \"$expected\" == \"$actual\" ]]; then\n pass \"$desc\"\n else\n fail \"$desc\" \"expected='$expected' actual='$actual'\"\n fi\n}\n\nassert_contains() {\n local desc=\"$1\" needle=\"$2\" haystack=\"$3\"\n if printf '%s' \"$haystack\" | grep -qF -- \"$needle\"; then\n pass \"$desc\"\n else\n fail \"$desc\" \"expected to contain: '$needle'\"\n fi\n}\n\nassert_not_contains() {\n local desc=\"$1\" needle=\"$2\" haystack=\"$3\"\n if ! printf '%s' \"$haystack\" | grep -qF -- \"$needle\"; then\n pass \"$desc\"\n else\n fail \"$desc\" \"expected NOT to contain: '$needle'\"\n fi\n}\n\nassert_file_exists() {\n local desc=\"$1\" path=\"$2\"\n if [[ -f \"$path\" ]]; then\n pass \"$desc\"\n else\n fail \"$desc\" \"file not found: $path\"\n fi\n}\n\n# ── Setup ─────────────────────────────────────────────────────────────────────\n\n# Source only the lib/tmux.sh functions needed for testing.\n# We must stub the real `tmux` binary before sourcing so that\n# psm_has_tmux (which calls `command -v tmux`) still works without a real tmux.\ntmux() { :; }\nsource \"${PSM_LIB_DIR}/tmux.sh\"\n\nTMPDIR_TEST=$(mktemp -d)\nTEMPLATE_TMP=\"${TMPDIR_TEST}/template.md\"\ncleanup() { rm -rf \"$TMPDIR_TEST\"; }\ntrap cleanup EXIT\n\n# ── psm_render_template ───────────────────────────────────────────────────────\n\necho \"\"\necho \"=== psm_render_template ===\"\n\n# 1. Simple single substitution\nprintf 'Hello {{NAME}}!' > \"$TEMPLATE_TMP\"\nresult=$(psm_render_template \"$TEMPLATE_TMP\" \"NAME=World\")\nassert_equals \"renders single variable\" \"Hello World!\" \"$result\"\n\n# 2. Multiple variables in one call\nprintf 'PR #{{PR_NUMBER}}: {{PR_TITLE}} by @{{PR_AUTHOR}}' > \"$TEMPLATE_TMP\"\nresult=$(psm_render_template \"$TEMPLATE_TMP\" \"PR_NUMBER=123\" \"PR_TITLE=Fix bug\" \"PR_AUTHOR=alice\")\nassert_equals \"renders multiple variables\" \"PR #123: Fix bug by @alice\" \"$result\"\n\n# 3. Unreferenced placeholder is preserved\nprintf 'Hello {{NAME}} and {{OTHER}}!' > \"$TEMPLATE_TMP\"\nresult=$(psm_render_template \"$TEMPLATE_TMP\" \"NAME=World\")\nassert_contains \"preserves unreferenced placeholders\" \"{{OTHER}}\" \"$result\"\nassert_not_contains \"replaces referenced placeholder\" \"{{NAME}}\" \"$result\"\n\n# 4. Missing template file returns non-zero and error message\nset +e\nerr_out=$(psm_render_template \"/nonexistent/missing.md\" \"KEY=val\" 2>&1)\nexit_code=$?\nset -e\n[[ $exit_code -ne 0 ]] && pass \"returns non-zero for missing template\" \\\n || fail \"returns non-zero for missing template\" \"exited 0\"\nassert_contains \"error message for missing template\" \"error|\" \"$err_out\"\n\n# 5. Value containing forward slash (common in branch names)\nprintf '{{BRANCH}}' > \"$TEMPLATE_TMP\"\nresult=$(psm_render_template \"$TEMPLATE_TMP\" \"BRANCH=feature/my-branch\")\nassert_equals \"handles slashes in values\" \"feature/my-branch\" \"$result\"\n\n# 6. Empty value clears placeholder\nprintf 'before {{EMPTY}} after' > \"$TEMPLATE_TMP\"\nresult=$(psm_render_template \"$TEMPLATE_TMP\" \"EMPTY=\")\nassert_equals \"empty value clears placeholder\" \"before after\" \"$result\"\n\n# 7. Value with & (must not be interpreted as backreference)\nprintf '{{VAL}}' > \"$TEMPLATE_TMP\"\nresult=$(psm_render_template \"$TEMPLATE_TMP\" \"VAL=foo&bar\")\nassert_equals \"handles ampersand in value\" \"foo&bar\" \"$result\"\n\n# ── _psm_pane_has_claude_prompt ───────────────────────────────────────────────\n\necho \"\"\necho \"=== _psm_pane_has_claude_prompt ===\"\n\n# 8. Detects bare '>' prompt\nif _psm_pane_has_claude_prompt \">\"; then\n pass \"detects bare '>' prompt\"\nelse\n fail \"detects bare '>' prompt\" \"returned non-zero\"\nfi\n\n# 9. Detects '> ' with trailing space\nif _psm_pane_has_claude_prompt \"> \"; then\n pass \"detects '> ' with trailing space\"\nelse\n fail \"detects '> ' with trailing space\" \"returned non-zero\"\nfi\n\n# 10. Detects '?' trust prompt\nif _psm_pane_has_claude_prompt \"?\"; then\n pass \"detects '?' trust prompt\"\nelse\n fail \"detects '?' trust prompt\" \"returned non-zero\"\nfi\n\n# 11. Detects '>' with leading whitespace\nif _psm_pane_has_claude_prompt \" > \"; then\n pass \"detects indented '>' prompt\"\nelse\n fail \"detects indented '>' prompt\" \"returned non-zero\"\nfi\n\n# 12. Does NOT match '>' that appears mid-line\nif ! _psm_pane_has_claude_prompt \"Loading > modules\"; then\n pass \"rejects mid-line >\"\nelse\n fail \"rejects mid-line >\" \"should have returned non-zero\"\nfi\n\n# 13. Does NOT match empty string\nif ! _psm_pane_has_claude_prompt \"\"; then\n pass \"rejects empty pane\"\nelse\n fail \"rejects empty pane\" \"should have returned non-zero\"\nfi\n\n# 14. Does NOT match arbitrary text\nif ! _psm_pane_has_claude_prompt \"Welcome to Claude Code!\"; then\n pass \"rejects non-prompt welcome text\"\nelse\n fail \"rejects non-prompt welcome text\" \"should have returned non-zero\"\nfi\n\n# ── psm_launch_claude with mocked tmux/inject ─────────────────────────────────\n\necho \"\"\necho \"=== psm_launch_claude (mocked) ===\"\n\n# Mock state\nTMUX_SEND_CALLS=()\nINJECT_CALLED=false\nINJECT_SESSION=\"\"\nINJECT_FILE=\"\"\n\n# Override tmux to record calls; simulate has-session returning 0\ntmux() {\n local subcmd=\"$1\"\n case \"$subcmd\" in\n has-session) return 0 ;;\n send-keys) TMUX_SEND_CALLS+=(\"$*\") ;;\n *) : ;;\n esac\n}\n\n# Override psm_inject_prompt to track invocations\npsm_inject_prompt() {\n INJECT_CALLED=true\n INJECT_SESSION=\"$1\"\n INJECT_FILE=\"$2\"\n return 0\n}\n\n# 15. Without context file: inject is NOT called\nTMUX_SEND_CALLS=(); INJECT_CALLED=false\npsm_launch_claude \"test-session\"\nif [[ \"$INJECT_CALLED\" == \"false\" ]]; then\n pass \"no inject without context file\"\nelse\n fail \"no inject without context file\" \"psm_inject_prompt was called unexpectedly\"\nfi\n\n# 16. With context file: inject IS called\nTMUX_SEND_CALLS=(); INJECT_CALLED=false; INJECT_SESSION=\"\"; INJECT_FILE=\"\"\npsm_launch_claude \"test-session\" \".psm/review.md\"\nif [[ \"$INJECT_CALLED\" == \"true\" ]]; then\n pass \"inject called when context file provided\"\nelse\n fail \"inject called when context file provided\" \"psm_inject_prompt was NOT called\"\nfi\n\n# 17. inject receives correct session name\nassert_equals \"inject gets correct session name\" \"test-session\" \"$INJECT_SESSION\"\n\n# 18. inject receives correct context file path\nassert_equals \"inject gets correct context file path\" \".psm/review.md\" \"$INJECT_FILE\"\n\n# 19. Result reports 'launched|'\nTMUX_SEND_CALLS=(); INJECT_CALLED=false\nresult=$(psm_launch_claude \"test-session\" \".psm/review.md\")\nassert_contains \"launch reports launched| on success\" \"launched|\" \"$result\"\n\n# 20. Session-not-found returns error (has-session fails)\ntmux() {\n case \"$1\" in\n has-session) return 1 ;;\n *) : ;;\n esac\n}\n# Use || true so set -e doesn't kill the script on the expected non-zero return\nresult=$(psm_launch_claude \"missing-session\" 2>&1) || true\nassert_contains \"error reported for missing session\" \"error|\" \"$result\"\n\n# ── Context file creation (cmd_review simulation) ─────────────────────────────\n\necho \"\"\necho \"=== Context file creation (cmd_review simulation) ===\"\n\nWORKTREE_TMP=\"${TMPDIR_TEST}/worktree\"\nmkdir -p \"$WORKTREE_TMP\"\n\n# Variables that cmd_review would supply\nt_pr_number=\"123\"\nt_pr_title=\"Fix null pointer in auth\"\nt_pr_author=\"alice\"\nt_pr_url=\"https://github.com/test/repo/pull/123\"\nt_head_branch=\"fix/auth-null\"\nt_base_branch=\"main\"\nt_pr_body=\"Fixes the NPE in AuthService when token is missing.\"\nt_changed_files=\"src/auth.ts\nsrc/auth.test.ts\"\n\ncontext_rel=\".psm/review.md\"\ncontext_file=\"${WORKTREE_TMP}/${context_rel}\"\nmkdir -p \"$(dirname \"$context_file\")\"\n\npsm_render_template \"${TEMPLATES_DIR}/pr-review.md\" \\\n \"PR_NUMBER=${t_pr_number}\" \\\n \"PR_TITLE=${t_pr_title}\" \\\n \"PR_AUTHOR=${t_pr_author}\" \\\n \"PR_URL=${t_pr_url}\" \\\n \"HEAD_BRANCH=${t_head_branch}\" \\\n \"BASE_BRANCH=${t_base_branch}\" \\\n \"PR_BODY=${t_pr_body}\" \\\n \"CHANGED_FILES=${t_changed_files}\" \\\n > \"$context_file\"\n\n# 21. Context file exists in worktree\nassert_file_exists \"review context file written to worktree\" \"$context_file\"\n\ncontext_content=$(cat \"$context_file\")\n\n# 22-27. All placeholders resolved with correct values\nassert_contains \"PR number in rendered context\" \"$t_pr_number\" \"$context_content\"\nassert_contains \"PR title in rendered context\" \"$t_pr_title\" \"$context_content\"\nassert_contains \"PR author in rendered context\" \"$t_pr_author\" \"$context_content\"\nassert_contains \"PR URL in rendered context\" \"$t_pr_url\" \"$context_content\"\nassert_contains \"head branch in rendered context\" \"$t_head_branch\" \"$context_content\"\nassert_contains \"base branch in rendered context\" \"$t_base_branch\" \"$context_content\"\n\n# 28-31. No unreplaced placeholders remain\nassert_not_contains \"no {{PR_NUMBER}} leftover\" \"{{PR_NUMBER}}\" \"$context_content\"\nassert_not_contains \"no {{PR_TITLE}} leftover\" \"{{PR_TITLE}}\" \"$context_content\"\nassert_not_contains \"no {{PR_AUTHOR}} leftover\" \"{{PR_AUTHOR}}\" \"$context_content\"\nassert_not_contains \"no {{HEAD_BRANCH}} leftover\" \"{{HEAD_BRANCH}}\" \"$context_content\"\n\n# ── Context file creation (cmd_fix simulation) ────────────────────────────────\n\necho \"\"\necho \"=== Context file creation (cmd_fix simulation) ===\"\n\nt_issue_number=\"42\"\nt_issue_title=\"Auth fails on token expiry\"\nt_issue_url=\"https://github.com/test/repo/issues/42\"\nt_issue_labels=\"bug, auth\"\nt_issue_body=\"Users get 500 errors after token expires.\"\nt_branch_name=\"fix/42-auth-token-expiry\"\n\nfix_context_rel=\".psm/fix.md\"\nfix_context_file=\"${WORKTREE_TMP}/${fix_context_rel}\"\nmkdir -p \"$(dirname \"$fix_context_file\")\"\n\npsm_render_template \"${TEMPLATES_DIR}/issue-fix.md\" \\\n \"ISSUE_NUMBER=${t_issue_number}\" \\\n \"ISSUE_TITLE=${t_issue_title}\" \\\n \"ISSUE_URL=${t_issue_url}\" \\\n \"ISSUE_LABELS=${t_issue_labels}\" \\\n \"ISSUE_BODY=${t_issue_body}\" \\\n \"BRANCH_NAME=${t_branch_name}\" \\\n > \"$fix_context_file\"\n\n# 32. Fix context file exists\nassert_file_exists \"fix context file written to worktree\" \"$fix_context_file\"\n\nfix_content=$(cat \"$fix_context_file\")\n\n# 33-35. Key values present\nassert_contains \"issue number in fix context\" \"$t_issue_number\" \"$fix_content\"\nassert_contains \"issue title in fix context\" \"$t_issue_title\" \"$fix_content\"\nassert_contains \"branch name in fix context\" \"$t_branch_name\" \"$fix_content\"\n\n# 36-38. No unreplaced placeholders\nassert_not_contains \"no {{ISSUE_NUMBER}} leftover\" \"{{ISSUE_NUMBER}}\" \"$fix_content\"\nassert_not_contains \"no {{ISSUE_TITLE}} leftover\" \"{{ISSUE_TITLE}}\" \"$fix_content\"\nassert_not_contains \"no {{BRANCH_NAME}} leftover\" \"{{BRANCH_NAME}}\" \"$fix_content\"\n\n# ── Summary ──────────────────────────────────────────────────────────────────\n\necho \"\"\necho \"Results: ${PASS} passed, ${FAIL} failed\"\n[[ $FAIL -eq 0 ]] && exit 0 || exit 1\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":12189,"content_sha256":"df26c165a77e905c94a7e0bc0ec96df68fd1e5603f8b08faccadc89b5668553e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Project Session Manager (PSM) Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"psm","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the compatibility alias for this canonical skill entrypoint.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Quick Start (worktree-first):","type":"text","marks":[{"type":"strong"}]},{"text":" Start with ","type":"text"},{"text":"omc teleport","type":"text","marks":[{"type":"code_inline"}]},{"text":" when you want an isolated issue/PR/feature worktree before adding any tmux/session orchestration:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"omc teleport #123 # Create worktree for issue/PR\nomc teleport my-feature # Create worktree for feature\nomc teleport list # List worktrees","type":"text"}]},{"type":"paragraph","content":[{"text":"See ","type":"text"},{"text":"Teleport Command","type":"text","marks":[{"type":"link","attrs":{"href":"#teleport-command","title":null}}]},{"text":" below for details.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Automate isolated development environments using git worktrees and tmux sessions with Claude Code. Enables parallel work across multiple tasks, projects, and repositories.","type":"text"}]},{"type":"paragraph","content":[{"text":"Canonical slash command: ","type":"text"},{"text":"/oh-my-claudecode:project-session-manager","type":"text","marks":[{"type":"code_inline"}]},{"text":" (alias: ","type":"text"},{"text":"/oh-my-claudecode:psm","type":"text","marks":[{"type":"code_inline"}]},{"text":").","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Commands","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Example","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"review \u003cref>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PR review session","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm review omc#123","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fix \u003cref>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Issue fix session","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm fix omc#42","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"feature \u003cproj> \u003cname>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature development","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm feature omc add-webhooks","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"list [project]","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"List active sessions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm list","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"attach \u003csession>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Attach to session","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm attach omc:pr-123","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"kill \u003csession>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Kill session","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm kill omc:pr-123","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cleanup","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Clean merged/closed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm cleanup","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Current session info","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/psm status","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Project References","type":"text"}]},{"type":"paragraph","content":[{"text":"Supported formats:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Alias","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"omc#123","type":"text","marks":[{"type":"code_inline"}]},{"text":" (requires ","type":"text"},{"text":"~/.psm/projects.json","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Full","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"owner/repo#123","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"URL","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"https://github.com/owner/repo/pull/123","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Current","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"#123","type":"text","marks":[{"type":"code_inline"}]},{"text":" (uses current directory's repo)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Configuration","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Project Aliases (","type":"text"},{"text":"~/.psm/projects.json","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"aliases\": {\n \"omc\": {\n \"repo\": \"Yeachan-Heo/oh-my-claudecode\",\n \"local\": \"~/Workspace/oh-my-claudecode\",\n \"default_base\": \"main\"\n }\n },\n \"defaults\": {\n \"worktree_root\": \"~/.psm/worktrees\",\n \"cleanup_after_days\": 14\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Providers","type":"text"}]},{"type":"paragraph","content":[{"text":"PSM supports multiple issue tracking providers:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Provider","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CLI Required","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Reference Formats","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Commands","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GitHub (default)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"gh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"owner/repo#123","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"alias#123","type":"text","marks":[{"type":"code_inline"}]},{"text":", GitHub URLs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"review, fix, feature","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Jira","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"jira","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PROJ-123","type":"text","marks":[{"type":"code_inline"}]},{"text":" (if PROJ configured), ","type":"text"},{"text":"alias#123","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fix, feature","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Jira Configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"To use Jira, add an alias with ","type":"text"},{"text":"jira_project","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"provider: \"jira\"","type":"text","marks":[{"type":"code_inline"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"aliases\": {\n \"mywork\": {\n \"jira_project\": \"MYPROJ\",\n \"repo\": \"mycompany/my-project\",\n \"local\": \"~/Workspace/my-project\",\n \"default_base\": \"develop\",\n \"provider\": \"jira\"\n }\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Important:","type":"text","marks":[{"type":"strong"}]},{"text":" The ","type":"text"},{"text":"repo","type":"text","marks":[{"type":"code_inline"}]},{"text":" field is still required for cloning the git repository. Jira tracks issues, but you work in a git repo.","type":"text"}]},{"type":"paragraph","content":[{"text":"For non-GitHub repos, use ","type":"text"},{"text":"clone_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" instead:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"aliases\": {\n \"private\": {\n \"jira_project\": \"PRIV\",\n \"clone_url\": \"[email protected]:team/repo.git\",\n \"local\": \"~/Workspace/repo\",\n \"provider\": \"jira\"\n }\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Jira Reference Detection","type":"text"}]},{"type":"paragraph","content":[{"text":"PSM only recognizes ","type":"text"},{"text":"PROJ-123","type":"text","marks":[{"type":"code_inline"}]},{"text":" format as Jira when ","type":"text"},{"text":"PROJ","type":"text","marks":[{"type":"code_inline"}]},{"text":" is explicitly configured as a ","type":"text"},{"text":"jira_project","type":"text","marks":[{"type":"code_inline"}]},{"text":" in your aliases. This prevents false positives from branch names like ","type":"text"},{"text":"FIX-123","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Jira Examples","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Fix a Jira issue (MYPROJ must be configured)\npsm fix MYPROJ-123\n\n# Fix using alias (recommended)\npsm fix mywork#123\n\n# Feature development (works same as GitHub)\npsm feature mywork add-webhooks\n\n# Note: 'psm review' is not supported for Jira (no PR concept)\n# Use 'psm fix' for Jira issues","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Jira CLI Setup","type":"text"}]},{"type":"paragraph","content":[{"text":"Install the Jira CLI:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# macOS\nbrew install ankitpokhrel/jira-cli/jira-cli\n\n# Linux\n# See: https://github.com/ankitpokhrel/jira-cli#installation\n\n# Configure (interactive)\njira init","type":"text"}]},{"type":"paragraph","content":[{"text":"The Jira CLI handles authentication separately from PSM.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Directory Structure","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"~/.psm/\n├── projects.json # Project aliases\n├── sessions.json # Active session registry\n└── worktrees/ # Worktree storage\n └── \u003cproject>/\n └── \u003ctype>-\u003cid>/","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Session Naming","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"public session ID","type":"text","marks":[{"type":"strong"}]},{"text":" (colon form, e.g. ","type":"text"},{"text":"omc:pr-123","type":"text","marks":[{"type":"code_inline"}]},{"text":") is the human-facing identifier stored in ","type":"text"},{"text":"sessions.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" and used with ","type":"text"},{"text":"psm attach","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"psm kill","type":"text","marks":[{"type":"code_inline"}]},{"text":". tmux reserves ","type":"text"},{"text":":","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":".","type":"text","marks":[{"type":"code_inline"}]},{"text":" for its ","type":"text"},{"text":"session:window.pane","type":"text","marks":[{"type":"code_inline"}]},{"text":" target syntax and silently rewrites them, so the ","type":"text"},{"text":"actual tmux session name","type":"text","marks":[{"type":"strong"}]},{"text":" uses a tmux-safe form where those characters become ","type":"text"},{"text":"_","type":"text","marks":[{"type":"code_inline"}]},{"text":" (issue #3528). PSM translates the public ID to the tmux-safe name at every tmux boundary; attach directly with the tmux-safe name.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Public ID (","type":"text"},{"text":"psm attach","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"kill","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tmux Session (","type":"text"},{"text":"tmux attach -t","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Worktree Dir","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PR Review","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"omc:pr-123","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"psm_omc_pr-123","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~/.psm/worktrees/omc/pr-123","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Issue Fix","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"omc:issue-42","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"psm_omc_issue-42","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~/.psm/worktrees/omc/issue-42","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"omc:feat-auth","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"psm_omc_feat-auth","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~/.psm/worktrees/omc/feat-auth","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Implementation Protocol","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user invokes a PSM command, follow this protocol:","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Parse Arguments","type":"text"}]},{"type":"paragraph","content":[{"text":"Parse ","type":"text"},{"text":"{{ARGUMENTS}}","type":"text","marks":[{"type":"code_inline"}]},{"text":" to determine:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Subcommand","type":"text","marks":[{"type":"strong"}]},{"text":": review, fix, feature, list, attach, kill, cleanup, status","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Reference","type":"text","marks":[{"type":"strong"}]},{"text":": project#number, URL, or session ID","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Options","type":"text","marks":[{"type":"strong"}]},{"text":": --branch, --base, --no-claude, --no-tmux, etc.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"review \u003cref>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Create PR review session","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolve reference","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Read project aliases\ncat ~/.psm/projects.json 2>/dev/null || echo '{\"aliases\":{}}'\n\n# Parse ref format: alias#num, owner/repo#num, or URL\n# Extract: project_alias, repo (owner/repo), pr_number, local_path","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fetch PR info","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gh pr view \u003cpr_number> --repo \u003crepo> --json number,title,author,headRefName,baseRefName,body,files,url","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ensure local repo exists","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# If local path doesn't exist, clone\nif [[ ! -d \"$local_path\" ]]; then\n git clone \"https://github.com/$repo.git\" \"$local_path\"\nfi","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create worktree","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"worktree_path=\"$HOME/.psm/worktrees/$project_alias/pr-$pr_number\"\n\n# Fetch PR branch\ncd \"$local_path\"\ngit fetch origin \"pull/$pr_number/head:pr-$pr_number-review\"\n\n# Create worktree\ngit worktree add \"$worktree_path\" \"pr-$pr_number-review\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create session metadata","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cat > \"$worktree_path/.psm-session.json\" \u003c\u003c EOF\n{\n \"id\": \"$project_alias:pr-$pr_number\",\n \"type\": \"review\",\n \"project\": \"$project_alias\",\n \"ref\": \"pr-$pr_number\",\n \"branch\": \"\u003chead_branch>\",\n \"base\": \"\u003cbase_branch>\",\n \"created_at\": \"$(date -Iseconds)\",\n \"tmux_session\": \"psm_${project_alias}_pr-$pr_number\",\n \"worktree_path\": \"$worktree_path\",\n \"source_repo\": \"$local_path\",\n \"github\": {\n \"pr_number\": $pr_number,\n \"pr_title\": \"\u003ctitle>\",\n \"pr_author\": \"\u003cauthor>\",\n \"pr_url\": \"\u003curl>\"\n },\n \"state\": \"active\"\n}\nEOF","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update sessions registry","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Add to ~/.psm/sessions.json","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create tmux session","type":"text","marks":[{"type":"strong"}]},{"text":" (tmux-safe name; ","type":"text"},{"text":":","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":".","type":"text","marks":[{"type":"code_inline"}]},{"text":" are translated to ","type":"text"},{"text":"_","type":"text","marks":[{"type":"code_inline"}]},{"text":"):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux new-session -d -s \"psm_${project_alias}_pr-$pr_number\" -c \"$worktree_path\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Launch Claude Code","type":"text","marks":[{"type":"strong"}]},{"text":" (unless --no-claude):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# --dangerously-skip-permissions prevents the \"Do you trust this directory?\" prompt\n# and repeated tool-approval prompts from stalling the session (issue #2508).\ntmux send-keys -t \"psm_${project_alias}_pr-$pr_number\" \"claude --dangerously-skip-permissions\" Enter\n\n# After claude boots (PSM_CLAUDE_STARTUP_DELAY, default 5s), deliver the task.\n# Use -l (literal) so special characters are not misinterpreted by tmux.\nsleep \"${PSM_CLAUDE_STARTUP_DELAY:-5}\"\ntmux send-keys -t \"psm_${project_alias}_pr-$pr_number\" -l \\\n \"Review PR #$pr_number: \\\"$pr_title\\\" by @$pr_author ($head_branch → $base_branch). URL: $pr_url.\" Enter","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Output session info","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Session ready!\n\n ID: omc:pr-123\n Worktree: ~/.psm/worktrees/omc/pr-123\n Tmux: psm_omc_pr-123\n\nTo attach: tmux attach -t psm_omc_pr-123 (or: psm attach omc:pr-123)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"fix \u003cref>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Create issue fix session","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolve reference","type":"text","marks":[{"type":"strong"}]},{"text":" (same as review)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fetch issue info","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gh issue view \u003cissue_number> --repo \u003crepo> --json number,title,body,labels,url","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create feature branch","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cd \"$local_path\"\ngit fetch origin main\nbranch_name=\"fix/$issue_number-$(echo \"$title\" | tr ' ' '-' | tr '[:upper:]' '[:lower:]' | head -c 30)\"\ngit checkout -b \"$branch_name\" origin/main","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create worktree","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"worktree_path=\"$HOME/.psm/worktrees/$project_alias/issue-$issue_number\"\ngit worktree add \"$worktree_path\" \"$branch_name\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create session metadata","type":"text","marks":[{"type":"strong"}]},{"text":" (similar to review, type=\"fix\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update registry, create tmux, launch claude","type":"text","marks":[{"type":"strong"}]},{"text":": Same as review, but pass issue context as the initial task prompt:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux send-keys -t \"psm_${project_alias}_issue-$issue_number\" \"claude --dangerously-skip-permissions\" Enter\n# After claude boots, deliver the task (see PSM_CLAUDE_STARTUP_DELAY):\ntmux send-keys -t \"psm_${project_alias}_issue-$issue_number\" -l \\\n \"Fix issue #$issue_number: \\\"$issue_title\\\". URL: $issue_url. Branch: $branch_name.\" Enter","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"feature \u003cproject> \u003cname>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Start feature development","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolve project","type":"text","marks":[{"type":"strong"}]},{"text":" (from alias or path)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create feature branch","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cd \"$local_path\"\ngit fetch origin main\nbranch_name=\"feature/$feature_name\"\ngit checkout -b \"$branch_name\" origin/main","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create worktree","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"worktree_path=\"$HOME/.psm/worktrees/$project_alias/feat-$feature_name\"\ngit worktree add \"$worktree_path\" \"$branch_name\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Create session, tmux, launch claude","type":"text","marks":[{"type":"strong"}]},{"text":" with feature context as initial prompt:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux send-keys -t \"psm_${project_alias}_feat-$feature_name\" \"claude --dangerously-skip-permissions\" Enter\ntmux send-keys -t \"psm_${project_alias}_feat-$feature_name\" -l \\\n \"Implement feature \\\"$feature_name\\\" for project $project. Branch: $branch_name.\" Enter","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"list [project]","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": List active sessions","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read sessions registry","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cat ~/.psm/sessions.json 2>/dev/null || echo '{\"sessions\":{}}'","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check tmux sessions","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux list-sessions -F \"#{session_name}\" 2>/dev/null | grep \"^psm_\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check worktrees","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"ls -la ~/.psm/worktrees/*/ 2>/dev/null","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Format output","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Active PSM Sessions:\n\nID | Type | Status | Worktree\n-------------------|---------|----------|---------------------------\nomc:pr-123 | review | active | ~/.psm/worktrees/omc/pr-123\nomc:issue-42 | fix | detached | ~/.psm/worktrees/omc/issue-42","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"attach \u003csession>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Attach to existing session","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Parse session ID","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"project:type-number","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify session exists","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux has-session -t \"psm_${session_id//[.:]/_}\" 2>/dev/null # translate public id to tmux-safe name","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Attach","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux attach -t \"psm_${session_id//[.:]/_}\"","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"kill \u003csession>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Kill session and cleanup","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Kill tmux session","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux kill-session -t \"psm_${session_id//[.:]/_}\" 2>/dev/null","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Remove worktree","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"worktree_path=$(jq -r \".sessions[\\\"$session_id\\\"].worktree\" ~/.psm/sessions.json)\nsource_repo=$(jq -r \".sessions[\\\"$session_id\\\"].source_repo\" ~/.psm/sessions.json)\n\ncd \"$source_repo\"\ngit worktree remove \"$worktree_path\" --force","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update registry","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Remove from sessions.json","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"cleanup","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Clean up merged PRs and closed issues","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read all sessions","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For each PR session, check if merged","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gh pr view \u003cpr_number> --repo \u003crepo> --json merged,state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For each issue session, check if closed","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"gh issue view \u003cissue_number> --repo \u003crepo> --json closed,state","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Clean up merged/closed sessions","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Kill tmux session","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Remove worktree","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Update registry","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Cleanup complete:\n Removed: omc:pr-123 (merged)\n Removed: omc:issue-42 (closed)\n Kept: omc:feat-auth (active)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Subcommand: ","type":"text"},{"text":"status","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"Purpose","type":"text","marks":[{"type":"strong"}]},{"text":": Show current session info","type":"text"}]},{"type":"paragraph","content":[{"text":"Steps","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Detect current session","type":"text","marks":[{"type":"strong"}]},{"text":" from tmux or cwd:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"tmux display-message -p \"#{session_name}\" 2>/dev/null\n# or check if cwd is inside a worktree","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Read session metadata","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"cat .psm-session.json 2>/dev/null","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Show status","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Current Session: omc:pr-123\nType: review\nPR: #123 - Add webhook support\nBranch: feature/webhooks\nCreated: 2 hours ago","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Handling","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Worktree exists","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Offer: attach, recreate, or abort","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PR not found","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verify URL/number, check permissions","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No tmux","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Warn and skip session creation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No gh CLI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error with install instructions","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Teleport Command","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"omc teleport","type":"text","marks":[{"type":"code_inline"}]},{"text":" command provides a lightweight alternative to full PSM sessions. It creates git worktrees without tmux session management — ideal for quick, isolated development.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Usage","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Create worktree for an issue or PR\nomc teleport #123\nomc teleport owner/repo#123\nomc teleport https://github.com/owner/repo/issues/42\n\n# Create worktree for a feature\nomc teleport my-feature\n\n# List existing worktrees\nomc teleport list\n\n# Remove a worktree\nomc teleport remove issue/my-repo-123\nomc teleport remove --force feat/my-repo-my-feature","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Options","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Flag","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--worktree","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Create worktree (default, kept for compatibility)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"true","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--path \u003cpath>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Custom worktree root directory","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~/Workspace/omc-worktrees/","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--base \u003cbranch>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Base branch to create from","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"main","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output as JSON","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"false","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Worktree Layout","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"~/Workspace/omc-worktrees/\n├── issue/\n│ └── my-repo-123/ # Issue worktrees\n├── pr/\n│ └── my-repo-456/ # PR review worktrees\n└── feat/\n └── my-repo-my-feature/ # Feature worktrees","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"PSM vs Teleport","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feature","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PSM","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Teleport","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Git worktree","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tmux session","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Claude Code launch","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Session registry","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto-cleanup","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Project aliases","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yes","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No (uses current repo)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"PSM","type":"text","marks":[{"type":"strong"}]},{"text":" for full managed sessions. Use ","type":"text"},{"text":"teleport","type":"text","marks":[{"type":"strong"}]},{"text":" for quick worktree creation.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Requirements","type":"text"}]},{"type":"paragraph","content":[{"text":"Required:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"git","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Version control (with worktree support v2.5+)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"jq","type":"text","marks":[{"type":"code_inline"}]},{"text":" - JSON parsing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"tmux","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Session management (optional, but recommended)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Optional (per provider):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"gh","type":"text","marks":[{"type":"code_inline"}]},{"text":" - GitHub CLI (for GitHub workflows)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"jira","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Jira CLI (for Jira workflows)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Initialization","type":"text"}]},{"type":"paragraph","content":[{"text":"On first run, create default config:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"mkdir -p ~/.psm/worktrees ~/.psm/logs\n\n# Create default projects.json if not exists\nif [[ ! -f ~/.psm/projects.json ]]; then\n cat > ~/.psm/projects.json \u003c\u003c 'EOF'\n{\n \"aliases\": {\n \"omc\": {\n \"repo\": \"Yeachan-Heo/oh-my-claudecode\",\n \"local\": \"~/Workspace/oh-my-claudecode\",\n \"default_base\": \"main\"\n }\n },\n \"defaults\": {\n \"worktree_root\": \"~/.psm/worktrees\",\n \"cleanup_after_days\": 14,\n \"auto_cleanup_merged\": true\n }\n}\nEOF\nfi\n\n# Create sessions.json if not exists\nif [[ ! -f ~/.psm/sessions.json ]]; then\n echo '{\"version\":1,\"sessions\":{},\"stats\":{\"total_created\":0,\"total_cleaned\":0}}' > ~/.psm/sessions.json\nfi","type":"text"}]}]},"metadata":{"date":"2026-11-02","name":"project-session-manager","level":2,"author":"@skillopedia","source":{"stars":40515,"repo_name":"oh-my-claudecode","origin_url":"https://github.com/Yeachan-Heo/oh-my-claudecode/blob/HEAD/skills/project-session-manager/SKILL.md","repo_owner":"Yeachan-Heo","body_sha256":"e5ab819e7fcf4aee63f55569f5f84199cb9ed7d9b9d2e88e5ee56059e0acf72c","cluster_key":"d3f3e12538aed9a357bf99ecf9d664e1a60d3c388a6a06c5efc750fad55ff596","clean_bundle":{"format":"clean-skill-bundle-v1","source":"Yeachan-Heo/oh-my-claudecode/skills/project-session-manager/SKILL.md","text_bytes":93290,"source_commit":"14cd0fe92fbe376fab6c89b333b7896811986d2d","skills_sh_rank":14026,"attachment_count":19,"text_attachments":19,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/project-session-manager/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"devops-infrastructure","category_label":"DevOps"},"exact_dupes_collapsed_into_this":0},"aliases":["psm"],"version":"v1","category":"devops-infrastructure","import_tag":"skills-sh-top20k-2026-11","description":"Worktree-first dev environment manager for issues, PRs, and features with optional tmux sessions"}},"renderedAt":1793666071226}

Project Session Manager (PSM) Skill is the compatibility alias for this canonical skill entrypoint. Quick Start (worktree-first): Start with when you want an isolated issue/PR/feature worktree before adding any tmux/session orchestration: See Teleport Command below for details. Automate isolated development environments using git worktrees and tmux sessions with Claude Code. Enables parallel work across multiple tasks, projects, and repositories. Canonical slash command: (alias: ). Commands | Command | Description | Example | |---------|-------------|---------| | | PR review session | | | | I…