feat: initial commit with orchestrator scripts and dev instructions

Import all 18 production orchestrator scripts from ~/. These are direct
copies with hardcoded paths — generalization is the next step. Includes
CLAUDE.local.md with project overview, architecture notes, and roadmap.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Prateek 2026-02-13 15:25:31 +05:30
commit 0273e8f3d0
19 changed files with 3460 additions and 0 deletions

110
CLAUDE.local.md Normal file
View File

@ -0,0 +1,110 @@
# CLAUDE.local.md - Agent Orchestrator Development
You are working on **agent-orchestrator**, an open-source multi-agent orchestrator for Claude Code. This tool manages parallel coding agents across projects from a single control plane.
> **Dog-fooding**: This orchestrator is being built using a copy of itself. The `~/` home directory orchestrator manages sessions that work on this very repo.
## Project Overview
Agent Orchestrator lets you:
- Spawn and manage dozens of Claude Code agents working in parallel
- Track progress via a live HTML dashboard with PR status, CI checks, and merge actions
- Send commands to agents, kill/cleanup sessions, and view terminals in-browser
- Organize agents by project with worktree-based isolation
## Repo Structure
```
agent-orchestrator/
├── scripts/ # Core orchestrator scripts
│ ├── claude-status # Unified dashboard (CLI)
│ ├── claude-batch-spawn # Spawn multiple sessions at once
│ ├── claude-spawn # Spawn single session in new terminal tab
│ ├── claude-dashboard # HTML dashboard with live PR status (web UI)
│ ├── claude-open-all # Open terminal tabs for all sessions
│ ├── claude-review-check # Trigger agents to address PR review comments
│ ├── claude-bugbot-fix # Fix bugbot comments across sessions
│ ├── claude-session-status # Health monitor (working/idle/blocked/waiting)
│ ├── claude-spawn-with-context # Spawn with custom prompt file
│ ├── claude-spawn-on-branch # Spawn on existing branch
│ ├── claude-spawn-with-prompt # Spawn and deliver prompt after ready
│ ├── get-claude-session-info # Extract session metadata from tmux
│ ├── open-tmux-session # Switch to terminal tab for tmux session
│ ├── open-iterm-tab # iTerm2 tab management
│ ├── notify-session # iTerm2 notifications from sessions
│ ├── send-to-session # Smart message delivery to tmux sessions
│ ├── claude-integrator-session # Example: project session manager (Linear/integrator)
│ └── claude-splitly-session # Example: project session manager (GitHub Issues/SafeSplit)
├── CLAUDE.local.md # This file (dev instructions)
├── CLAUDE.md # Repo-level instructions for contributors
└── README.md # Project README
```
## Current State
The scripts in `scripts/` are direct copies from the production orchestrator (`~/`). They currently have hardcoded paths and project-specific references (integrator, splitly). The work ahead is to:
1. **Generalize** — Remove hardcoded project names, make scripts project-agnostic
2. **Configuration** — Add a config file (e.g., `orchestrator.yaml`) that defines projects, repos, branches, issue trackers
3. **Installation** — Create an install script that symlinks scripts to `~/` or adds them to PATH
4. **Documentation** — Write a comprehensive README with setup guide and examples
5. **Self-hosting** — Make this repo use its own orchestrator for development
## Project Info
- **Repo**: ComposioHQ/agent-orchestrator (GitHub)
- **Issue Tracker**: Linear
- **Visibility**: Internal (ComposioHQ org)
- **Everything runs locally** — no Datadog, no cloud infra
## Development Workflow
```bash
# This repo is managed by the home directory orchestrator
# Sessions are spawned via:
~/claude-spawn-with-context i TICKET /tmp/prompt.txt --open
# Linear tickets for this project
# (use Rube MCP LINEAR_CREATE_LINEAR_ISSUE to create tickets)
# GitHub issues (secondary):
gh issue list --repo ComposioHQ/agent-orchestrator --state open
```
## Key Design Principles
1. **tmux-based** — All agent sessions run in tmux. This gives persistence, detach/attach, and scriptability.
2. **Metadata files** — Each session has a flat metadata file (`key=value` format) tracking branch, PR, issue, status, notes.
3. **Live dashboard** — HTML dashboard served locally with real-time activity detection via Claude's JSONL session files.
4. **Project-agnostic shared scripts** — Core scripts (spawn, status, dashboard) take project as an argument.
5. **Project-specific session managers** — Each project gets a session manager script that handles worktrees, naming, and cleanup.
6. **iTerm2 integration** — AppleScript-based tab management for macOS (should be made terminal-agnostic).
## Architecture Notes
### Session Lifecycle
```
spawn → tmux session created → Claude started → working on issue
metadata file written (branch, issue, status)
agent creates PR → metadata updated (pr=URL)
dashboard shows PR status, CI, review state
PR merged → cleanup kills session, archives metadata
```
### Activity Detection
The dashboard detects if agents are working/idle/exited by:
1. Checking Claude's JSONL session file modification time and last message type
2. Walking the process tree from tmux pane PID to find `claude` processes
3. Polling every 5 seconds via `/api/sessions` endpoint
### Dashboard Server
- Python HTTP server serving static HTML + JSON API
- `/api/sessions` — live activity status
- `/api/terminal/:name` — spawn ttyd web terminal for session
- `/api/kill/:name` — kill session + close iTerm tab + archive metadata
- `/api/merge/:repo/:num` — merge PR via `gh`
- `/api/send/:name` — send message to agent via `send-to-session`

183
scripts/claude-batch-spawn Executable file
View File

@ -0,0 +1,183 @@
#!/bin/bash
# Claude Batch Session Spawner (v2 - worktree-based)
# Delegates to session managers for worktree creation, symlinks, and MCP setup.
# Adds duplicate detection: skips issues that already have active sessions.
# Usage: ~/claude-batch-spawn <project> <issue1> [issue2] [issue3] ...
PROJECT="${1:-}"
shift
ISSUES=("$@")
if [ -z "$PROJECT" ] || [ ${#ISSUES[@]} -eq 0 ]; then
echo "Usage: ~/claude-batch-spawn <project> <issue1> [issue2] [issue3] ..."
echo ""
echo "Examples:"
echo " ~/claude-batch-spawn splitly 298 299 300 301"
echo " ~/claude-batch-spawn integrator INT-1020 INT-1021"
echo ""
echo "This creates worktree-based sessions for each issue."
echo "After creation, attach with: ~/claude-*-session attach <session-name>"
exit 1
fi
# Determine project settings
case "$PROJECT" in
integrator|i)
SESSION_PREFIX="integrator"
SESSION_MANAGER="$HOME/claude-integrator-session"
SESSION_DATA_DIR="$HOME/.integrator-sessions"
GITHUB_REPO=""
;;
splitly|s|safesplit)
SESSION_PREFIX="splitly"
SESSION_MANAGER="$HOME/claude-splitly-session"
SESSION_DATA_DIR="$HOME/.splitly-sessions"
GITHUB_REPO="UniverseOfThings/SafeSplit"
;;
*)
echo "Unknown project: $PROJECT"
echo "Use 'integrator' (or 'i') or 'splitly' (or 's')"
exit 1
;;
esac
mkdir -p "$SESSION_DATA_DIR"
# Duplicate detection: check if an issue already has an active session
issue_has_session() {
local issue="$1"
local issue_lower=$(echo "$issue" | tr '[:upper:]' '[:lower:]')
for meta_file in "$SESSION_DATA_DIR"/*; do
[ -f "$meta_file" ] || continue
local session_name=$(basename "$meta_file")
# Check if tmux session is still active
tmux has-session -t "$session_name" 2>/dev/null || continue
# Check issue field in metadata
local meta_issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
local meta_issue_lower=$(echo "$meta_issue" | tr '[:upper:]' '[:lower:]')
# Match by issue ID in the URL or direct match
if echo "$meta_issue_lower" | grep -qi "$issue_lower"; then
echo "$session_name"
return 0
fi
done
return 1
}
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ BATCH SESSION SPAWNER (v2) ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "Project: $PROJECT"
echo "Issues to spawn: ${ISSUES[*]}"
echo ""
CREATED=()
SKIPPED=()
FAILED=()
for ISSUE in "${ISSUES[@]}"; do
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Duplicate detection
existing=$(issue_has_session "$ISSUE")
if [ $? -eq 0 ]; then
echo " ⏭️ Skipping $ISSUE — already has session: $existing"
SKIPPED+=("$ISSUE:$existing")
continue
fi
echo " Creating session for: $ISSUE"
# Delegate to the session manager
output=$("$SESSION_MANAGER" new "$ISSUE" 2>&1)
if [ $? -ne 0 ]; then
echo " ❌ Error creating session!"
echo "$output" | sed 's/^/ /'
FAILED+=("$ISSUE")
continue
fi
# Extract session name from session manager output
SESSION=$(echo "$output" | grep "^SESSION=" | cut -d= -f2)
if [ -z "$SESSION" ]; then
echo " ❌ Error: Could not determine session name!"
echo "$output" | sed 's/^/ /'
FAILED+=("$ISSUE")
continue
fi
# Update metadata with issue info
if [ "$SESSION_PREFIX" = "splitly" ]; then
ISSUE_NUM=$(echo "$ISSUE" | sed 's/#//' | grep -o '[0-9]*')
ISSUE_URL="https://github.com/$GITHUB_REPO/issues/$ISSUE_NUM"
else
ISSUE_URL="$ISSUE"
fi
# Append issue to metadata (session manager may have already set it)
grep -q "^issue=" "$SESSION_DATA_DIR/$SESSION" 2>/dev/null || \
echo "issue=$ISSUE_URL" >> "$SESSION_DATA_DIR/$SESSION"
# Wait for Claude to start, then send the work prompt
sleep 1
# Build initial prompt
if [ "$SESSION_PREFIX" = "splitly" ]; then
INITIAL_PROMPT="Please start working on GitHub issue #$ISSUE_NUM. First, fetch the issue details using: gh issue view $ISSUE_NUM --repo $GITHUB_REPO. Then create an appropriate feature branch (e.g., fix/issue-$ISSUE_NUM-short-description or feat/issue-$ISSUE_NUM-short-description), and start implementing the solution."
else
INITIAL_PROMPT="Please start working on $ISSUE, fetch ticket info, create the appropriate branch so that github auto links to linear, and start working on the task"
fi
# Send prompt to the running Claude session
tmux send-keys -t "$SESSION" "$INITIAL_PROMPT" Enter
echo " ✅ Session $SESSION created and prompted"
CREATED+=("$SESSION:$ISSUE")
# Small delay between spawns
sleep 0.5
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 SUMMARY"
echo " Created: ${#CREATED[@]} sessions"
echo " Skipped: ${#SKIPPED[@]} (duplicate)"
echo " Failed: ${#FAILED[@]}"
echo ""
if [ ${#CREATED[@]} -gt 0 ]; then
echo "Created sessions:"
for item in "${CREATED[@]}"; do
SESSION=$(echo "$item" | cut -d: -f1)
ISSUE=$(echo "$item" | cut -d: -f2-)
echo " • $SESSION → $ISSUE"
done
fi
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo ""
echo "Skipped (already has session):"
for item in "${SKIPPED[@]}"; do
ISSUE=$(echo "$item" | cut -d: -f1)
EXISTING=$(echo "$item" | cut -d: -f2-)
echo " • $ISSUE → existing session: $EXISTING"
done
fi
if [ ${#FAILED[@]} -gt 0 ]; then
echo ""
echo "Failed issues: ${FAILED[*]}"
fi
echo ""
echo "To see all sessions: ~/claude-status"
echo "To open all tabs: ~/claude-open-all $SESSION_PREFIX"

88
scripts/claude-bugbot-fix Executable file
View File

@ -0,0 +1,88 @@
#!/bin/bash
# Check open PRs for unaddressed bugbot (cursor[bot]) comments
# and send fix instructions to the relevant sessions.
#
# Usage: ~/claude-bugbot-fix [project] [--dry-run]
#
# Examples:
# ~/claude-bugbot-fix integrator # Check and send fix instructions
# ~/claude-bugbot-fix integrator --dry-run # Just report, don't send
# ~/claude-bugbot-fix splitly
PROJECT="${1:-integrator}"
DRY_RUN=false
[ "$2" = "--dry-run" ] && DRY_RUN=true
case "$PROJECT" in
integrator|i)
REPO="ComposioHQ/integrator"
SESSIONS_DIR="$HOME/.integrator-sessions"
SESSION_PREFIX="integrator"
;;
splitly|s|safesplit)
REPO="UniverseOfThings/SafeSplit"
SESSIONS_DIR="$HOME/.splitly-sessions"
SESSION_PREFIX="splitly"
;;
*)
echo "Usage: $0 [integrator|splitly] [--dry-run]"
exit 1
;;
esac
echo "Checking $REPO for bugbot comments..."
echo ""
# Build PR → session map from metadata
declare -A PR_SESSION_MAP
for s in $(ls "$SESSIONS_DIR" 2>/dev/null | grep "^${SESSION_PREFIX}-"); do
pr_num=$(grep "^pr=" "$SESSIONS_DIR/$s" 2>/dev/null | grep -oE "[0-9]+" | tail -1)
[ -n "$pr_num" ] && PR_SESSION_MAP[$pr_num]="$s"
done
# Get all open PRs
OPEN_PRS=$(gh pr list --repo "$REPO" --state open --json number -q '.[].number' --limit 50)
FOUND=0
for pr in $OPEN_PRS; do
# Check if latest cursor[bot] review reports issues
latest_review=$(gh api "repos/$REPO/pulls/$pr/reviews" --jq '[.[] | select(.user.login == "cursor[bot]")] | last | .body // ""' 2>/dev/null)
if echo "$latest_review" | grep -q "potential issue"; then
issue_count=$(echo "$latest_review" | grep -oE "found [0-9]+" | grep -oE "[0-9]+")
session="${PR_SESSION_MAP[$pr]:-none}"
# Get the actual inline comments
review_id=$(gh api "repos/$REPO/pulls/$pr/reviews" --jq '[.[] | select(.user.login == "cursor[bot]")] | last | .id' 2>/dev/null)
comments=""
if [ -n "$review_id" ]; then
comments=$(gh api "repos/$REPO/pulls/$pr/reviews/$review_id/comments" --jq '.[] | "- [\(.path)] \(.body | split("\n")[0] | gsub("### "; ""))"' 2>/dev/null)
fi
echo "PR #$pr: $issue_count bugbot issue(s) → session: $session"
echo "$comments" | head -5
echo ""
FOUND=$((FOUND + 1))
# Send fix instruction if session exists and not dry-run
if [ "$session" != "none" ] && [ "$DRY_RUN" = "false" ]; then
if tmux has-session -t "$session" 2>/dev/null; then
# Build a concise fix message
comment_summary=$(gh api "repos/$REPO/pulls/$pr/reviews/$review_id/comments" --jq '.[] | "(\(.path | split("/") | last)): \(.body | split("\n")[0] | gsub("### "; ""))"' 2>/dev/null | tr '\n' '; ')
tmux send-keys -t "$session" "You have $issue_count unaddressed bugbot comment(s) on PR #$pr. Please run: gh api repos/$REPO/pulls/$pr/comments --jq '.[] | select(.user.login == \"cursor[bot]\") | .body' to see full details. Issues: $comment_summary Fix and push." Enter
echo " → Sent fix instruction to $session"
else
echo " → Session $session not running"
fi
fi
fi
done
if [ "$FOUND" -eq 0 ]; then
echo "No unaddressed bugbot comments found."
fi
echo ""
echo "Done. $FOUND PR(s) with bugbot issues."

1371
scripts/claude-dashboard Executable file

File diff suppressed because it is too large Load Diff

242
scripts/claude-integrator-session Executable file
View File

@ -0,0 +1,242 @@
#!/bin/bash
# claude-integrator-session-v2 - Worktree-based session manager for integrator
set -e
MAIN_REPO=~/integrator
WORKTREE_DIR=~/.worktrees/integrator
SESSION_DIR=~/.integrator-sessions
ENV_TEMPLATE=~/.env-templates/integrator
DEFAULT_BRANCH="next"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
usage() {
echo "Usage: ~/claude-integrator-session-v2 <command> [args]"
echo ""
echo "Commands:"
echo " new [LINEAR-ID] [--open] Create new session (optionally with Linear ticket)"
echo " ls List all sessions"
echo " attach <session> Attach to session"
echo " kill <session> Kill session and remove worktree"
echo " cleanup Kill sessions where PR is merged or ticket is done"
echo " update Update current session metadata"
exit 1
}
get_next_session_number() {
local max=0
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^integrator-[0-9]*$" | sed 's/integrator-//'); do
if [ "$session" -gt "$max" ] 2>/dev/null; then
max=$session
fi
done
echo $((max + 1))
}
cmd_new() {
local linear_id=""
local open_warp=false
# Parse args
for arg in "$@"; do
if [ "$arg" = "--open" ]; then
open_warp=true
elif [ -z "$linear_id" ] && [ "$arg" != "--open" ]; then
linear_id="$arg"
fi
done
local session_num=$(get_next_session_number)
local session_name="integrator-$session_num"
local worktree_path="$WORKTREE_DIR/$session_name"
echo -e "${BLUE}Creating session: $session_name${NC}"
cd "$MAIN_REPO"
git fetch origin --quiet
if [ -n "$linear_id" ]; then
local branch_name="feat/$linear_id"
git worktree add -b "$branch_name" "$worktree_path" "origin/$DEFAULT_BRANCH" 2>/dev/null || \
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH"
else
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH" --detach
fi
echo -e "${GREEN}✓ Created worktree at $worktree_path${NC}"
# Symlink shared resources from main repo
[ -f "$ENV_TEMPLATE/.env" ] && ln -sf "$ENV_TEMPLATE/.env" "$worktree_path/.env"
[ -f "$ENV_TEMPLATE/.envrc" ] && ln -sf "$ENV_TEMPLATE/.envrc" "$worktree_path/.envrc" && \
(cd "$worktree_path" && direnv allow 2>/dev/null || true)
[ -d "$MAIN_REPO/.venv" ] && ln -sf "$MAIN_REPO/.venv" "$worktree_path/.venv"
[ -f "$MAIN_REPO/CLAUDE.local.md" ] && ln -sf "$MAIN_REPO/CLAUDE.local.md" "$worktree_path/CLAUDE.local.md"
[ -d "$MAIN_REPO/.claude" ] && rm -rf "$worktree_path/.claude" && ln -s "$MAIN_REPO/.claude" "$worktree_path/.claude"
# Add Rube MCP before starting session
(cd "$worktree_path" && claude mcp add rube --transport http https://rube.app/mcp &>/dev/null) || true
# Create tmux session and start Claude (unset CLAUDECODE to avoid nested session detection)
tmux new-session -d -s "$session_name" -c "$worktree_path" -e "INTEGRATOR_SESSION=$session_name" -e "DIRENV_LOG_FORMAT="
tmux send-keys -t "$session_name" "unset CLAUDECODE && claude --dangerously-skip-permissions" Enter
# Create metadata file
mkdir -p "$SESSION_DIR"
cat > "$SESSION_DIR/$session_name" << EOF
worktree=$worktree_path
branch=$(cd "$worktree_path" && git branch --show-current || echo "detached")
status=starting
EOF
[ -n "$linear_id" ] && echo "issue=https://linear.app/composio/issue/$linear_id" >> "$SESSION_DIR/$session_name"
echo -e "${GREEN}✓ Created tmux session: $session_name${NC}"
echo -e "${GREEN}✓ Started Claude in session${NC}"
echo ""
echo "Attach with: tmux attach -t $session_name"
echo "Or open in iTerm2: ~/open-iterm-tab $session_name"
# Clean output for scripting (no ANSI codes)
echo "SESSION=$session_name"
# Open in iTerm2 if requested
if [ "$open_warp" = true ]; then
~/open-iterm-tab "$session_name"
fi
}
cmd_ls() {
echo -e "${BLUE}Integrator Sessions:${NC}"
echo ""
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^integrator-" | sort -V); do
local meta_file="$SESSION_DIR/$session"
local worktree="" branch="" status="" pr=""
if [ -f "$meta_file" ]; then
worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
branch=$(grep "^branch=" "$meta_file" 2>/dev/null | cut -d= -f2-)
status=$(grep "^status=" "$meta_file" 2>/dev/null | cut -d= -f2-)
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
fi
[ -d "$worktree" ] && branch=$(cd "$worktree" && git branch --show-current 2>/dev/null || echo "$branch")
echo -e "${GREEN}$session${NC}"
[ -n "$branch" ] && echo " Branch: $branch"
[ -n "$status" ] && echo " Status: $status"
[ -n "$pr" ] && echo " PR: $pr"
echo ""
done
}
cmd_attach() {
[ -z "$1" ] && echo "Usage: ~/claude-integrator-session-v2 attach <session>" && exit 1
tmux attach -t "$1"
}
cmd_kill() {
local session="$1"
[ -z "$session" ] && echo "Usage: ~/claude-integrator-session-v2 kill <session>" && exit 1
local meta_file="$SESSION_DIR/$session"
local worktree=""
[ -f "$meta_file" ] && worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
# Close iTerm tab BEFORE killing tmux (tab name changes after tmux dies)
osascript -e "
tell application \"iTerm2\"
repeat with w in windows
repeat with t in tabs of w
repeat with s in sessions of t
if name of s contains \"$session\" then
close t
return
end if
end repeat
end repeat
end repeat
end tell
" 2>/dev/null && echo -e "${GREEN}✓ Closed iTerm tab: $session${NC}"
tmux kill-session -t "$session" 2>/dev/null && echo -e "${GREEN}✓ Killed tmux session: $session${NC}"
if [ -n "$worktree" ] && [ -d "$worktree" ]; then
cd "$MAIN_REPO"
git worktree remove --force "$worktree" 2>/dev/null && echo -e "${GREEN}✓ Removed worktree: $worktree${NC}"
fi
# Archive metadata instead of deleting
if [ -f "$meta_file" ]; then
mkdir -p "$SESSION_DIR/archive"
mv "$meta_file" "$SESSION_DIR/archive/${session}_$(date +%Y%m%d-%H%M%S)"
echo -e "${GREEN}✓ Archived metadata${NC}"
fi
}
cmd_cleanup() {
echo "Checking for completed sessions..."
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^integrator-"); do
local meta_file="$SESSION_DIR/$session"
local pr="" issue=""
[ -f "$meta_file" ] && {
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
}
if [ -n "$pr" ]; then
local pr_num=$(echo "$pr" | grep -o '[0-9]*$')
if [ -n "$pr_num" ]; then
local state=$(gh pr view "$pr_num" --repo ComposioHQ/integrator --json state -q '.state' 2>/dev/null)
[ "$state" = "MERGED" ] && echo -e "${YELLOW}PR #$pr_num merged - killing $session${NC}" && cmd_kill "$session" && continue
fi
fi
# Check Linear ticket status if we have an issue URL
if [ -n "$issue" ] && [ -n "${LINEAR_API_KEY:-}" ]; then
local ticket_id=$(echo "$issue" | grep -o 'INT-[0-9]*')
if [ -n "$ticket_id" ]; then
local ticket_state=$(curl -s -X POST https://api.linear.app/graphql \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"query\": \"{ issueSearch(filter: { identifier: { eq: \\\"$ticket_id\\\" } }) { nodes { state { type } } } }\"}" \
2>/dev/null | jq -r '.data.issueSearch.nodes[0].state.type // empty' 2>/dev/null)
if [ "$ticket_state" = "completed" ] || [ "$ticket_state" = "canceled" ]; then
echo -e "${YELLOW}$ticket_id $ticket_state - killing $session${NC}"
cmd_kill "$session" && continue
fi
fi
fi
done
echo "Cleanup complete."
}
cmd_update() {
local session="${INTEGRATOR_SESSION:-}"
[ -z "$session" ] && echo "Error: Not in an integrator session" && exit 1
local meta_file="$SESSION_DIR/$session"
local worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
[ -z "$worktree" ] || [ ! -d "$worktree" ] && worktree="$PWD"
local branch=$(cd "$worktree" && git branch --show-current 2>/dev/null)
[ -n "$branch" ] && sed -i '' "s|^branch=.*|branch=$branch|" "$meta_file" 2>/dev/null
echo "Updated metadata for $session"
}
case "${1:-}" in
new) shift; cmd_new "$@" ;;
ls) cmd_ls ;;
attach) cmd_attach "$2" ;;
kill) cmd_kill "$2" ;;
cleanup) cmd_cleanup ;;
update) cmd_update ;;
*) usage ;;
esac

133
scripts/claude-open-all Executable file
View File

@ -0,0 +1,133 @@
#!/bin/bash
# Open iTerm2 tabs for all active sessions (or a specific project)
# Usage: ~/claude-open-all [--new-window] [integrator|splitly|all]
NEW_WINDOW=false
PROJECT=""
# Parse arguments
for arg in "$@"; do
case "$arg" in
--new-window|-w)
NEW_WINDOW=true
;;
integrator|i|splitly|s|safesplit|all)
PROJECT="$arg"
;;
*)
echo "Unknown argument: $arg"
echo "Usage: ~/claude-open-all [--new-window] [integrator|splitly|all]"
exit 1
;;
esac
done
PROJECT="${PROJECT:-all}"
get_sessions() {
local prefix="$1"
tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${prefix}-" | sort
}
open_session_new_window() {
local session="$1"
osascript -e "
tell application \"iTerm2\"
activate
set newWindow to (create window with default profile)
tell current session of newWindow
set name to \"$session\"
write text \"printf '\\\\033]0;$session\\\\007' && tmux attach -t $session\"
end tell
end tell
"
}
open_session_new_tab() {
local session="$1"
osascript -e "
tell application \"iTerm2\"
activate
tell current window
create tab with default profile
tell current session
set name to \"$session\"
write text \"printf '\\\\033]0;$session\\\\007' && tmux attach -t $session\"
end tell
end tell
end tell
"
}
switch_to_existing_tab() {
local session="$1"
osascript -e "
tell application \"iTerm2\"
activate
repeat with aWindow in windows
repeat with aTab in tabs of aWindow
repeat with aSession in sessions of aTab
try
if profile name of aSession is equal to \"$session\" then
select aWindow
select aTab
return \"FOUND\"
end if
end try
end repeat
end repeat
end repeat
return \"NOT_FOUND\"
end tell
"
}
SESSIONS=()
case "$PROJECT" in
integrator|i)
SESSIONS=($(get_sessions "integrator"))
echo "Opening ${#SESSIONS[@]} integrator sessions..."
;;
splitly|s|safesplit)
SESSIONS=($(get_sessions "splitly"))
echo "Opening ${#SESSIONS[@]} splitly sessions..."
;;
all|"")
SESSIONS=($(get_sessions "integrator") $(get_sessions "splitly"))
echo "Opening ${#SESSIONS[@]} total sessions..."
;;
esac
if [ ${#SESSIONS[@]} -eq 0 ]; then
echo "No sessions found."
exit 0
fi
if [ "$NEW_WINDOW" = true ]; then
echo "(in new window)"
fi
echo ""
FIRST=true
for session in "${SESSIONS[@]}"; do
# Check if tab already exists
existing=$(switch_to_existing_tab "$session" 2>/dev/null)
if [ "$existing" = "FOUND" ]; then
echo " Switched to existing: $session"
elif [ "$NEW_WINDOW" = true ] && [ "$FIRST" = true ]; then
echo " Opening (new window): $session"
open_session_new_window "$session"
FIRST=false
else
echo " Opening (new tab): $session"
open_session_new_tab "$session"
fi
sleep 0.3
done
echo ""
echo "Done! Opened ${#SESSIONS[@]} iTerm2 tabs."
echo "Use Cmd+Shift+[ or ] to switch between tabs."

165
scripts/claude-review-check Executable file
View File

@ -0,0 +1,165 @@
#!/bin/bash
# Check all sessions with PRs for review comments and trigger agents to address them
# Usage: ~/claude-review-check [integrator|splitly]
PROJECT="${1:-splitly}"
case "$PROJECT" in
integrator|i)
SESSION_PREFIX="integrator"
SESSION_DATA_DIR="$HOME/.integrator-sessions"
;;
splitly|s|safesplit)
SESSION_PREFIX="splitly"
SESSION_DATA_DIR="$HOME/.splitly-sessions"
;;
*)
echo "Usage: ~/claude-review-check [integrator|splitly]"
exit 1
;;
esac
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ PR REVIEW COMMENT CHECKER ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
echo "Project: $PROJECT"
echo ""
# Get all sessions
sessions=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^${SESSION_PREFIX}-" | sort)
if [ -z "$sessions" ]; then
echo "No active sessions found."
exit 0
fi
TRIGGERED=()
SKIPPED_NO_PR=()
SKIPPED_NO_COMMENTS=()
ERRORS=()
for session in $sessions; do
data_file="$SESSION_DATA_DIR/$session"
if [ ! -f "$data_file" ]; then
SKIPPED_NO_PR+=("$session (no metadata)")
continue
fi
pr_url=$(grep "^pr=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
branch=$(grep "^branch=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
if [ -z "$pr_url" ]; then
SKIPPED_NO_PR+=("$session")
continue
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📍 $session"
echo " Branch: ${branch:--}"
echo " PR: $pr_url"
# Extract PR number and repo from URL
# Format: https://github.com/owner/repo/pull/123
if [[ "$pr_url" =~ github\.com/([^/]+)/([^/]+)/pull/([0-9]+) ]]; then
owner="${BASH_REMATCH[1]}"
repo="${BASH_REMATCH[2]}"
pr_number="${BASH_REMATCH[3]}"
else
echo " ⚠️ Could not parse PR URL"
ERRORS+=("$session: invalid PR URL")
continue
fi
# Check for review comments using gh
# Get pending review comments (not resolved)
echo " Checking for review comments..."
# Get PR review comments
review_data=$(gh pr view "$pr_number" --repo "$owner/$repo" --json reviewDecision,reviews,comments 2>/dev/null)
if [ $? -ne 0 ]; then
echo " ⚠️ Failed to fetch PR data"
ERRORS+=("$session: gh command failed")
continue
fi
# Check review decision
review_decision=$(echo "$review_data" | jq -r '.reviewDecision // "NONE"')
# Count reviews that requested changes
changes_requested=$(echo "$review_data" | jq '[.reviews[] | select(.state == "CHANGES_REQUESTED")] | length')
# Get review comments (these are inline code comments)
# We'll use the gh api to get pending review comments
pending_comments=$(gh api "repos/$owner/$repo/pulls/$pr_number/comments" --jq '[.[] | select(.in_reply_to_id == null)] | length' 2>/dev/null || echo "0")
# Also check for regular PR comments that might be review feedback
pr_comments=$(gh pr view "$pr_number" --repo "$owner/$repo" --json comments --jq '.comments | length' 2>/dev/null || echo "0")
echo " Review decision: $review_decision"
echo " Review comments: $pending_comments"
echo " PR comments: $pr_comments"
# Determine if we need to trigger the agent
should_trigger=false
trigger_reason=""
if [ "$review_decision" = "CHANGES_REQUESTED" ]; then
should_trigger=true
trigger_reason="Changes requested"
elif [ "$changes_requested" -gt 0 ]; then
should_trigger=true
trigger_reason="$changes_requested review(s) requested changes"
elif [ "$pending_comments" -gt 0 ]; then
should_trigger=true
trigger_reason="$pending_comments review comment(s) to address"
fi
if [ "$should_trigger" = true ]; then
echo " 🔔 $trigger_reason - triggering agent..."
# Send prompt to the tmux session
prompt="There are review comments on your PR ($pr_url). Please check the PR review comments using 'gh pr view $pr_number --repo $owner/$repo --comments' and 'gh api repos/$owner/$repo/pulls/$pr_number/comments', address each comment, commit and push your fixes, then reply to the comments indicating what you fixed."
# Send to tmux - first send Ctrl+C to interrupt if busy, then the prompt
tmux send-keys -t "$session" C-c 2>/dev/null
sleep 0.5
tmux send-keys -t "$session" "$prompt" Enter
TRIGGERED+=("$session: $trigger_reason")
echo " ✅ Agent triggered"
else
echo " ✓ No pending review comments"
SKIPPED_NO_COMMENTS+=("$session")
fi
done
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 SUMMARY"
echo " Triggered: ${#TRIGGERED[@]}"
echo " No PR: ${#SKIPPED_NO_PR[@]}"
echo " No comments: ${#SKIPPED_NO_COMMENTS[@]}"
echo " Errors: ${#ERRORS[@]}"
if [ ${#TRIGGERED[@]} -gt 0 ]; then
echo ""
echo "Triggered sessions:"
for item in "${TRIGGERED[@]}"; do
echo " • $item"
done
fi
if [ ${#ERRORS[@]} -gt 0 ]; then
echo ""
echo "Errors:"
for item in "${ERRORS[@]}"; do
echo " • $item"
done
fi
echo ""

87
scripts/claude-session-status Executable file
View File

@ -0,0 +1,87 @@
#!/bin/bash
# Usage: ~/claude-session-status [session-pattern]
# Shows status of Claude sessions: working, idle, or blocked
PATTERN="${1:-integrator-}"
echo "Session Status Report - $(date '+%H:%M:%S')"
echo "==========================================="
echo ""
working=()
idle=()
blocked=()
not_running=()
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^$PATTERN" | sort -V); do
# Capture last 15 lines of the pane
output=$(tmux capture-pane -t "$session" -p -S -15 2>/dev/null)
if [[ -z "$output" ]]; then
not_running+=("$session")
continue
fi
# Check for active processing indicators
if echo "$output" | grep -qE "Thinking|Roosting|Garnishing|Levitating|Baking|Whirring|⏺|esc to interrupt"; then
# Get what it's doing (last status line)
status=$(echo "$output" | grep -oE "(Thinking|Roosting|Garnishing|Levitating|Baking|Whirring|Reading|Writing|Editing|Running).*" | tail -1 | cut -c1-50)
working+=("$session|${status:-processing...}")
# Check for prompt (idle/waiting for input)
elif echo "$output" | grep -qE "^\s*$|^$"; then
# Get last assistant message snippet
last_msg=$(echo "$output" | grep -v "^" | grep -v "^─" | grep -v "^$" | tail -1 | cut -c1-60)
idle+=("$session|${last_msg:-ready}")
# Check for error/blocked states
elif echo "$output" | grep -qiE "error|failed|permission denied|blocked|quota|rate limit"; then
error=$(echo "$output" | grep -iE "error|failed|permission denied|blocked|quota|rate limit" | tail -1 | cut -c1-60)
blocked+=("$session|${error:-unknown error}")
# Check for yes/no prompts or questions
elif echo "$output" | grep -qE "\[y/N\]|\[Y/n\]|Continue\?|Proceed\?"; then
blocked+=("$session|waiting for confirmation")
else
# Unknown state - probably idle
last_line=$(echo "$output" | grep -v "^$" | tail -1 | cut -c1-60)
idle+=("$session|${last_line:-unknown}")
fi
done
# Print results
if [[ ${#working[@]} -gt 0 ]]; then
echo "🔄 WORKING (${#working[@]})"
for item in "${working[@]}"; do
session="${item%%|*}"
status="${item#*|}"
printf " %-20s %s\n" "$session" "$status"
done
echo ""
fi
if [[ ${#idle[@]} -gt 0 ]]; then
echo "✅ IDLE/DONE (${#idle[@]})"
for item in "${idle[@]}"; do
session="${item%%|*}"
status="${item#*|}"
printf " %-20s %s\n" "$session" "$status"
done
echo ""
fi
if [[ ${#blocked[@]} -gt 0 ]]; then
echo "⚠️ BLOCKED/ERROR (${#blocked[@]})"
for item in "${blocked[@]}"; do
session="${item%%|*}"
status="${item#*|}"
printf " %-20s %s\n" "$session" "$status"
done
echo ""
fi
if [[ ${#not_running[@]} -gt 0 ]]; then
echo "💤 NOT RUNNING (${#not_running[@]})"
printf " %s\n" "${not_running[@]}"
echo ""
fi
echo "==========================================="
echo "Total: $((${#working[@]} + ${#idle[@]} + ${#blocked[@]})) active sessions"

119
scripts/claude-spawn Executable file
View File

@ -0,0 +1,119 @@
#!/bin/bash
# Claude Session Spawner
# Opens a new iTerm2 window and starts a Claude session
# Usage: ~/claude-spawn [integrator|splitly] [issue-url-or-number]
PROJECT="${1:-}"
ISSUE="${2:-}"
show_help() {
cat << 'EOF'
CLAUDE-SPAWN(1) User Commands CLAUDE-SPAWN(1)
NAME
claude-spawn - Spawn a new Claude session in a new iTerm2 window
SYNOPSIS
~/claude-spawn <project> [issue]
DESCRIPTION
Opens a new iTerm2 terminal window and starts a Claude Code session for the
specified project. Useful when you want to spawn work without leaving
your current terminal.
ARGUMENTS
project Required. One of:
integrator - Start an integrator session (Linear-based)
splitly - Start a splitly/SafeSplit session (GitHub-based)
i - Alias for integrator
s - Alias for splitly
issue Optional. Issue to work on:
- Linear URL for integrator (e.g., https://linear.app/team/issue/INT-123)
- GitHub issue number or URL for splitly (e.g., 294)
EXAMPLES
# Spawn a new integrator session (auto-picks checkout)
~/claude-spawn integrator
# Spawn integrator session for a Linear ticket
~/claude-spawn integrator https://linear.app/composio/issue/INT-1020
# Spawn splitly session for GitHub issue #294
~/claude-spawn splitly 294
# Using aliases
~/claude-spawn i INT-1020
~/claude-spawn s 294
REQUIREMENTS
- iTerm2 must be installed
SEE ALSO
~/claude-integrator-session-v2
~/claude-splitly-session-v2
~/claude-status
2026-02-01 CLAUDE-SPAWN(1)
EOF
}
# Handle help
if [[ "$PROJECT" == "help" || "$PROJECT" == "--help" || "$PROJECT" == "-h" ]]; then
show_help
exit 0
fi
# Validate project
case "$PROJECT" in
integrator|i)
SESSION_SCRIPT="$HOME/claude-integrator-session-v2"
PROJECT_NAME="integrator"
;;
splitly|s|safesplit)
SESSION_SCRIPT="$HOME/claude-splitly-session-v2"
PROJECT_NAME="splitly"
;;
"")
echo "Usage: ~/claude-spawn <integrator|splitly> [issue]"
echo ""
echo "Examples:"
echo " ~/claude-spawn integrator"
echo " ~/claude-spawn integrator https://linear.app/composio/issue/INT-1020"
echo " ~/claude-spawn splitly 294"
echo ""
echo "Run '~/claude-spawn help' for more information."
exit 1
;;
*)
echo "Unknown project: $PROJECT"
echo "Use 'integrator' (or 'i') or 'splitly' (or 's')"
exit 1
;;
esac
# Build the command to run in the new window
if [ -n "$ISSUE" ]; then
COMMAND="$SESSION_SCRIPT new '$ISSUE'"
else
COMMAND="$SESSION_SCRIPT new"
fi
echo "Spawning $PROJECT_NAME session in new iTerm2 window..."
if [ -n "$ISSUE" ]; then
echo " Issue: $ISSUE"
fi
# Open new iTerm2 window and run the command
osascript -e "
tell application \"iTerm2\"
activate
set newWindow to (create window with default profile)
tell current session of newWindow
write text \"$COMMAND\"
end tell
end tell
"
echo "Done! Check your iTerm2 windows."

77
scripts/claude-spawn-on-branch Executable file
View File

@ -0,0 +1,77 @@
#!/bin/bash
# Usage: ~/claude-spawn-on-branch <project> <branch> <prompt> [--open]
# Creates session on branch - reuses existing worktree if one exists for the branch
set -e
PROJECT="$1"
BRANCH="$2"
PROMPT="$3"
OPEN_TAB=false
[ "$4" = "--open" ] && OPEN_TAB=true
# Normalize project name
case "$PROJECT" in
i|integrator)
PROJECT="integrator"
WORKTREE_BASE=~/.worktrees/integrator
MAIN_REPO=~/secondary_checkouts/integrator-1
;;
s|splitly)
PROJECT="splitly"
WORKTREE_BASE=~/.worktrees/splitly
MAIN_REPO=~/projects/SafeSplit
;;
*) echo "Usage: ~/claude-spawn-on-branch <project> <branch> <prompt> [--open]"; exit 1 ;;
esac
[ -z "$BRANCH" ] || [ -z "$PROMPT" ] && { echo "Usage: ~/claude-spawn-on-branch <project> <branch> <prompt> [--open]"; exit 1; }
# Check if worktree already exists for this branch
cd "$MAIN_REPO"
git fetch origin -q
EXISTING_WORKTREE=$(git worktree list | grep "\[$BRANCH\]" | awk '{print $1}' | head -1)
if [ -n "$EXISTING_WORKTREE" ]; then
# Reuse existing worktree - name session as <owner>-N
OWNER_SESSION=$(basename "$EXISTING_WORKTREE")
# Find next sub-session number for this worktree
MAX_SUB=1
for s in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^${OWNER_SESSION}-[0-9]*$" | sed "s/${OWNER_SESSION}-//"); do
[ "$s" -gt "$MAX_SUB" ] 2>/dev/null && MAX_SUB=$s
done
SESSION="${OWNER_SESSION}-$((MAX_SUB + 1))"
tmux new-session -d -s "$SESSION" -c "$EXISTING_WORKTREE"
tmux send-keys -t "$SESSION" "claude --dangerously-skip-permissions" Enter
echo "Reusing worktree: $EXISTING_WORKTREE"
else
# Create new session (which creates new worktree)
output=$(~/claude-${PROJECT}-session new 2>&1)
SESSION=$(echo "$output" | grep "^SESSION=" | cut -d= -f2)
[ -z "$SESSION" ] && { echo "Failed to create session"; echo "$output"; exit 1; }
cd "$WORKTREE_BASE/$SESSION" && git checkout "$BRANCH" -q && git pull origin "$BRANCH" -q 2>/dev/null || true
fi
# Open tab if requested
[ "$OPEN_TAB" = true ] && ~/open-iterm-tab "$SESSION"
# Wait for Claude prompt
for i in {1..60}; do
if tmux capture-pane -t "$SESSION" -p 2>/dev/null | grep -q "^"; then
break
fi
sleep 0.5
done
sleep 0.2
tmux send-keys -t "$SESSION" "$PROMPT"
sleep 0.1
tmux send-keys -t "$SESSION" Enter
echo "✓ $SESSION on $BRANCH - prompt sent"

109
scripts/claude-spawn-with-context Executable file
View File

@ -0,0 +1,109 @@
#!/bin/bash
# Spawn a Claude session with custom context/prompt (v2 - worktree-based)
# Delegates to session managers for worktree creation, symlinks, and MCP setup.
# Usage: ~/claude-spawn-with-context <project> <ticket-id> <prompt-file> [--open]
#
# Examples:
# ~/claude-spawn-with-context integrator INT-1020 /tmp/prompt.txt --open
# ~/claude-spawn-with-context i INT-1020 /tmp/prompt.txt --open
# ~/claude-spawn-with-context splitly 295 /tmp/prompt.txt
# ~/claude-spawn-with-context s 295 /tmp/prompt.txt
set -e
# Parse args - support --open anywhere
PROJECT=""
TICKET=""
PROMPT_FILE=""
OPEN_TAB=false
for arg in "$@"; do
if [ "$arg" = "--open" ]; then
OPEN_TAB=true
elif [ -z "$PROJECT" ]; then
PROJECT="$arg"
elif [ -z "$TICKET" ]; then
TICKET="$arg"
elif [ -z "$PROMPT_FILE" ]; then
PROMPT_FILE="$arg"
fi
done
if [ -z "$PROJECT" ] || [ -z "$TICKET" ] || [ -z "$PROMPT_FILE" ]; then
echo "Usage: ~/claude-spawn-with-context <project> <ticket-id> <prompt-file> [--open]"
echo ""
echo " project: integrator (or i) / splitly (or s)"
echo " ticket-id: Linear ticket (INT-XXXX) or GitHub issue number"
echo " prompt-file: path to file containing the prompt"
echo " --open: open in new iTerm2 tab after spawning"
echo ""
echo "Examples:"
echo " ~/claude-spawn-with-context i INT-1020 /tmp/prompt.txt --open"
echo " ~/claude-spawn-with-context s 295 /tmp/prompt.txt"
exit 1
fi
if [ ! -f "$PROMPT_FILE" ]; then
echo "Error: prompt file not found: $PROMPT_FILE"
exit 1
fi
# Determine session manager
case "$PROJECT" in
integrator|i)
PROJECT="integrator"
SESSION_MANAGER="$HOME/claude-integrator-session"
SESSIONS_DIR="$HOME/.integrator-sessions"
;;
splitly|s|safesplit)
PROJECT="splitly"
SESSION_MANAGER="$HOME/claude-splitly-session"
SESSIONS_DIR="$HOME/.splitly-sessions"
;;
*)
echo "Error: unknown project '$PROJECT'. Use 'integrator' (i) or 'splitly' (s)."
exit 1
;;
esac
echo "Creating session via $PROJECT session manager..."
# Delegate session creation to the session manager (handles worktree, symlinks, MCP, tmux, Claude)
OPEN_FLAG=""
[ "$OPEN_TAB" = true ] && OPEN_FLAG="--open"
output=$("$SESSION_MANAGER" new "$TICKET" $OPEN_FLAG 2>&1)
echo "$output"
# Extract session name from manager output
SESSION=$(echo "$output" | grep "^SESSION=" | cut -d= -f2)
if [ -z "$SESSION" ]; then
echo "Error: Could not determine session name from session manager output!"
exit 1
fi
# Update metadata with ticket info
if [ "$PROJECT" = "integrator" ]; then
grep -q "^issue=" "$SESSIONS_DIR/$SESSION" 2>/dev/null || \
echo "issue=https://linear.app/composio/issue/$TICKET" >> "$SESSIONS_DIR/$SESSION"
grep -q "^summary=" "$SESSIONS_DIR/$SESSION" 2>/dev/null && \
sed -i '' "s|^summary=.*|summary=Starting work on $TICKET (custom context)|" "$SESSIONS_DIR/$SESSION" || \
echo "summary=Starting work on $TICKET (custom context)" >> "$SESSIONS_DIR/$SESSION"
else
grep -q "^issue=" "$SESSIONS_DIR/$SESSION" 2>/dev/null || \
echo "issue=https://github.com/UniverseOfThings/SafeSplit/issues/$TICKET" >> "$SESSIONS_DIR/$SESSION"
fi
# Wait for Claude to start, then send the custom prompt
sleep 2
# Use load-buffer + paste-buffer for multi-line prompts (handles special chars safely)
tmux load-buffer "$PROMPT_FILE"
tmux paste-buffer -t "$SESSION"
sleep 0.3
tmux send-keys -t "$SESSION" Enter
echo ""
echo "Custom prompt sent to $SESSION"
echo "Attach with: tmux attach -t $SESSION"
echo "Done!"

View File

@ -0,0 +1,32 @@
#!/bin/bash
# Usage: ~/claude-spawn-with-prompt <project> <prompt> [--open]
set -e
PROJECT="$1"; PROMPT="$2"; OPEN_TAB=false
[[ "$3" == "--open" ]] && OPEN_TAB=true
case "$PROJECT" in
i|integrator) SCRIPT=~/claude-integrator-session ;;
s|splitly) SCRIPT=~/claude-splitly-session ;;
*) echo "Usage: ~/claude-spawn-with-prompt <i|s> \"prompt\" [--open]"; exit 1 ;;
esac
[[ -z "$PROMPT" ]] && { echo "Usage: ~/claude-spawn-with-prompt <i|s> \"prompt\" [--open]"; exit 1; }
# Create session WITHOUT --open to avoid race condition
# We'll open the tab AFTER sending the message
SESSION=$($SCRIPT new 2>&1 | grep "^SESSION=" | cut -d= -f2)
[[ -z "$SESSION" ]] && { echo "Failed to create session"; exit 1; }
# Wait for Claude prompt to appear (up to 6 seconds)
for _ in {1..30}; do tmux capture-pane -t "$SESSION" -p 2>/dev/null | grep -q "^" && break; sleep 0.2; done
# Send the message
~/send-to-session "$SESSION" "$PROMPT"
# NOW open the iTerm tab if requested (after message is sent)
if [[ "$OPEN_TAB" == true ]]; then
~/open-iterm-tab "$SESSION"
fi
echo "✓ $SESSION"

216
scripts/claude-splitly-session Executable file
View File

@ -0,0 +1,216 @@
#!/bin/bash
# claude-splitly-session-v2 - Worktree-based session manager for splitly
set -e
MAIN_REPO=~/projects/SafeSplit
WORKTREE_DIR=~/.worktrees/splitly
SESSION_DIR=~/.splitly-sessions
ENV_TEMPLATE=~/.env-templates/splitly
DEFAULT_BRANCH="main"
GITHUB_REPO="UniverseOfThings/SafeSplit"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
usage() {
echo "Usage: ~/claude-splitly-session-v2 <command> [args]"
echo ""
echo "Commands:"
echo " new [ISSUE-NUM] [--open] Create new session (optionally with GitHub issue)"
echo " ls List all sessions"
echo " attach <session> Attach to session"
echo " kill <session> Kill session and remove worktree"
echo " cleanup Kill sessions where PR is merged or issue closed"
echo " update Update current session metadata"
exit 1
}
get_next_session_number() {
local max=0
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^splitly-[0-9]*$" | sed 's/splitly-//'); do
if [ "$session" -gt "$max" ] 2>/dev/null; then
max=$session
fi
done
echo $((max + 1))
}
cmd_new() {
local issue_num=""
local open_warp=false
for arg in "$@"; do
if [ "$arg" = "--open" ]; then
open_warp=true
elif [ -z "$issue_num" ] && [ "$arg" != "--open" ]; then
issue_num="$arg"
fi
done
local session_num=$(get_next_session_number)
local session_name="splitly-$session_num"
local worktree_path="$WORKTREE_DIR/$session_name"
echo -e "${BLUE}Creating session: $session_name${NC}"
cd "$MAIN_REPO"
git fetch origin --quiet
if [ -n "$issue_num" ]; then
local branch_name="feat/issue-$issue_num"
git worktree add -b "$branch_name" "$worktree_path" "origin/$DEFAULT_BRANCH" 2>/dev/null || \
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH"
else
git worktree add "$worktree_path" "origin/$DEFAULT_BRANCH" --detach
fi
echo -e "${GREEN}✓ Created worktree at $worktree_path${NC}"
# Symlink env files
[ -f "$ENV_TEMPLATE/.env" ] && ln -sf "$ENV_TEMPLATE/.env" "$worktree_path/.env"
[ -f "$ENV_TEMPLATE/.envrc" ] && ln -sf "$ENV_TEMPLATE/.envrc" "$worktree_path/.envrc" && \
(cd "$worktree_path" && direnv allow 2>/dev/null || true)
# Symlink .claude directory for MCP access (remove if exists as dir)
[ -d "$MAIN_REPO/.claude" ] && rm -rf "$worktree_path/.claude" && ln -s "$MAIN_REPO/.claude" "$worktree_path/.claude"
# Add Rube MCP before starting session
(cd "$worktree_path" && claude mcp add rube --transport http https://rube.app/mcp &>/dev/null) || true
# Create tmux session and start Claude (unset CLAUDECODE to avoid nested session detection)
tmux new-session -d -s "$session_name" -c "$worktree_path" -e "SPLITLY_SESSION=$session_name" -e "DIRENV_LOG_FORMAT="
tmux send-keys -t "$session_name" "unset CLAUDECODE && claude --dangerously-skip-permissions" Enter
# Create metadata file
mkdir -p "$SESSION_DIR"
cat > "$SESSION_DIR/$session_name" << EOF
worktree=$worktree_path
branch=$(cd "$worktree_path" && git branch --show-current || echo "detached")
status=starting
EOF
[ -n "$issue_num" ] && echo "issue=$issue_num" >> "$SESSION_DIR/$session_name"
echo -e "${GREEN}✓ Created tmux session: $session_name${NC}"
echo -e "${GREEN}✓ Started Claude in session${NC}"
echo ""
echo "Attach with: tmux attach -t $session_name"
echo "Or open in iTerm2: ~/open-iterm-tab $session_name"
# Clean output for scripting
echo "SESSION=$session_name"
if [ "$open_warp" = true ]; then
~/open-iterm-tab "$session_name"
fi
}
cmd_ls() {
echo -e "${BLUE}Splitly Sessions:${NC}"
echo ""
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^splitly-" | sort -V); do
local meta_file="$SESSION_DIR/$session"
local worktree="" branch="" status="" pr="" issue=""
if [ -f "$meta_file" ]; then
worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
branch=$(grep "^branch=" "$meta_file" 2>/dev/null | cut -d= -f2-)
status=$(grep "^status=" "$meta_file" 2>/dev/null | cut -d= -f2-)
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
fi
[ -d "$worktree" ] && branch=$(cd "$worktree" && git branch --show-current 2>/dev/null || echo "$branch")
echo -e "${GREEN}$session${NC}"
[ -n "$branch" ] && echo " Branch: $branch"
[ -n "$issue" ] && echo " Issue: #$issue"
[ -n "$status" ] && echo " Status: $status"
[ -n "$pr" ] && echo " PR: $pr"
echo ""
done
}
cmd_attach() {
[ -z "$1" ] && echo "Usage: ~/claude-splitly-session-v2 attach <session>" && exit 1
tmux attach -t "$1"
}
cmd_kill() {
local session="$1"
[ -z "$session" ] && echo "Usage: ~/claude-splitly-session-v2 kill <session>" && exit 1
local meta_file="$SESSION_DIR/$session"
local worktree=""
[ -f "$meta_file" ] && worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
tmux kill-session -t "$session" 2>/dev/null && echo -e "${GREEN}✓ Killed tmux session: $session${NC}"
if [ -n "$worktree" ] && [ -d "$worktree" ]; then
cd "$MAIN_REPO"
git worktree remove --force "$worktree" 2>/dev/null && echo -e "${GREEN}✓ Removed worktree: $worktree${NC}"
fi
# Archive metadata instead of deleting
if [ -f "$meta_file" ]; then
mkdir -p "$SESSION_DIR/archive"
mv "$meta_file" "$SESSION_DIR/archive/${session}_$(date +%Y%m%d-%H%M%S)"
echo -e "${GREEN}✓ Archived metadata${NC}"
fi
}
cmd_cleanup() {
echo "Checking for completed sessions..."
for session in $(tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^splitly-"); do
local meta_file="$SESSION_DIR/$session"
local pr="" issue=""
[ -f "$meta_file" ] && {
pr=$(grep "^pr=" "$meta_file" 2>/dev/null | cut -d= -f2-)
issue=$(grep "^issue=" "$meta_file" 2>/dev/null | cut -d= -f2-)
}
if [ -n "$pr" ]; then
local pr_num=$(echo "$pr" | grep -o '[0-9]*$')
if [ -n "$pr_num" ]; then
local state=$(gh pr view "$pr_num" --repo "$GITHUB_REPO" --json state -q '.state' 2>/dev/null)
[ "$state" = "MERGED" ] && echo -e "${YELLOW}PR #$pr_num merged - killing $session${NC}" && cmd_kill "$session" && continue
fi
fi
if [ -n "$issue" ]; then
local issue_state=$(gh issue view "$issue" --repo "$GITHUB_REPO" --json state -q '.state' 2>/dev/null)
[ "$issue_state" = "CLOSED" ] && echo -e "${YELLOW}Issue #$issue closed - killing $session${NC}" && cmd_kill "$session"
fi
done
echo "Cleanup complete."
}
cmd_update() {
local session="${SPLITLY_SESSION:-}"
[ -z "$session" ] && echo "Error: Not in a splitly session" && exit 1
local meta_file="$SESSION_DIR/$session"
local worktree=$(grep "^worktree=" "$meta_file" 2>/dev/null | cut -d= -f2-)
[ -z "$worktree" ] || [ ! -d "$worktree" ] && worktree="$PWD"
local branch=$(cd "$worktree" && git branch --show-current 2>/dev/null)
[ -n "$branch" ] && sed -i '' "s|^branch=.*|branch=$branch|" "$meta_file" 2>/dev/null
echo "Updated metadata for $session"
}
case "${1:-}" in
new) shift; cmd_new "$@" ;;
ls) cmd_ls ;;
attach) cmd_attach "$2" ;;
kill) cmd_kill "$2" ;;
cleanup) cmd_cleanup ;;
update) cmd_update ;;
*) usage ;;
esac

212
scripts/claude-status Executable file
View File

@ -0,0 +1,212 @@
#!/bin/bash
# Claude Unified Status
# Shows all sessions across all projects with summary stats
# Helper function to get Claude session info directly from Claude's data
get_claude_info() {
local TMUX_SESSION="$1"
# Get the TTY for this tmux session
local TTY=$(tmux list-panes -t "$TMUX_SESSION" -F '#{pane_tty}' 2>/dev/null | head -1)
[ -z "$TTY" ] && return 1
# Find the Claude PID running on that TTY
local TTY_SHORT="${TTY#/dev/}"
local CLAUDE_PID=$(ps -eo pid,tty,comm 2>/dev/null | awk -v tty="$TTY_SHORT" '$2 == tty && $3 == "claude" {print $1; exit}')
[ -z "$CLAUDE_PID" ] && return 1
# Get the working directory of that process
local CWD=$(lsof -p "$CLAUDE_PID" 2>/dev/null | awk '$4 == "cwd" {print $NF}')
[ -z "$CWD" ] && return 1
# Get current git branch from that directory
local BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
# Convert to Claude's project path format and find active session
local PROJECT_PATH=$(echo "$CWD" | tr '/_' '--')
local PROJECT_DIR="$HOME/.claude/projects/$PROJECT_PATH"
local SESSION_ID=""
local SUMMARY=""
if [ -d "$PROJECT_DIR" ]; then
# Find the most recently modified session file (exclude agent- files)
local ACTIVE_SESSION_FILE=$(ls -t "$PROJECT_DIR"/*.jsonl 2>/dev/null | grep -v "agent-" | head -1)
if [ -n "$ACTIVE_SESSION_FILE" ]; then
SESSION_ID=$(basename "$ACTIVE_SESSION_FILE" .jsonl)
# Extract the most recent summary from the session file
SUMMARY=$(grep '"type":"summary"' "$ACTIVE_SESSION_FILE" 2>/dev/null | tail -1 | jq -r '.summary // empty' 2>/dev/null)
fi
fi
# Output as key=value pairs (properly quoted for eval)
printf 'claude_branch=%q\n' "$BRANCH"
printf 'claude_session_id=%q\n' "$SESSION_ID"
printf 'claude_summary=%q\n' "$SUMMARY"
}
echo "╔══════════════════════════════════════════════════════════════════════════════╗"
echo "║ CLAUDE ORCHESTRATOR STATUS ║"
echo "╚══════════════════════════════════════════════════════════════════════════════╝"
echo ""
# Count sessions
INTEGRATOR_COUNT=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^integrator-" || echo 0)
SPLITLY_COUNT=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^splitly-" || echo 0)
TOTAL=$((INTEGRATOR_COUNT + SPLITLY_COUNT))
# Count empty checkouts
INTEGRATOR_EMPTY=0
for i in {0..9}; do
count=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^integrator-${i}-" 2>/dev/null || true)
count=${count:-0}
count=$(echo "$count" | tr -d '[:space:]')
[ "$count" -eq 0 ] 2>/dev/null && ((INTEGRATOR_EMPTY++))
done
SPLITLY_EMPTY=0
for i in {0..12}; do
count=$(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep -c "^splitly-${i}-" 2>/dev/null || true)
count=${count:-0}
count=$(echo "$count" | tr -d '[:space:]')
[ "$count" -eq 0 ] 2>/dev/null && ((SPLITLY_EMPTY++))
done
echo "📊 SUMMARY: $TOTAL active sessions"
echo " Integrator: $INTEGRATOR_COUNT sessions, $INTEGRATOR_EMPTY empty checkouts"
echo " Splitly: $SPLITLY_COUNT sessions, $SPLITLY_EMPTY empty checkouts"
echo ""
# Show integrator sessions
echo "┌──────────────────────────────────────────────────────────────────────────────┐"
echo "│ INTEGRATOR │"
echo "└──────────────────────────────────────────────────────────────────────────────┘"
if [ "$INTEGRATOR_COUNT" -eq 0 ]; then
echo " (no active sessions)"
else
# Get session details from the session data files
for session in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^integrator-" | sort); do
activity=$(tmux display-message -t "$session" -p '#{session_activity}' 2>/dev/null)
now=$(date +%s)
if [ -n "$activity" ]; then
diff=$((now - activity))
if [ $diff -lt 60 ]; then
last_active="${diff}s ago"
elif [ $diff -lt 3600 ]; then
last_active="$((diff / 60))m ago"
elif [ $diff -lt 86400 ]; then
last_active="$((diff / 3600))h ago"
else
last_active="$((diff / 86400))d ago"
fi
else
last_active="-"
fi
data_file="$HOME/.integrator-sessions/$session"
meta_branch=$(grep "^branch=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
meta_status=$(grep "^status=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
meta_summary=$(grep "^summary=" "$data_file" 2>/dev/null | cut -d'=' -f2- | cut -c1-60)
# Get live Claude session info
eval "$(get_claude_info "$session" 2>/dev/null)"
echo " 📍 $session ($last_active)"
# Show branch - prefer live, show if different from metadata
if [ -n "$claude_branch" ]; then
if [ -n "$meta_branch" ] && [ "$claude_branch" != "$meta_branch" ]; then
echo " Branch: $claude_branch (meta: $meta_branch)"
else
echo " Branch: $claude_branch"
fi
elif [ -n "$meta_branch" ]; then
echo " Branch: $meta_branch (stale)"
fi
# Show summary - prefer Claude's auto-generated, fall back to metadata
if [ -n "$claude_summary" ]; then
echo " Claude: ${claude_summary:0:65}"
fi
if [ -n "$meta_summary" ]; then
echo " Status: ${meta_summary}..."
fi
# Show session ID if available (useful for resuming)
[ -n "$claude_session_id" ] && echo " Session: ${claude_session_id:0:8}..."
done
fi
echo ""
# Show splitly sessions
echo "┌──────────────────────────────────────────────────────────────────────────────┐"
echo "│ SPLITLY / SAFESPLIT │"
echo "└──────────────────────────────────────────────────────────────────────────────┘"
if [ "$SPLITLY_COUNT" -eq 0 ]; then
echo " (no active sessions)"
else
for session in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | grep "^splitly-" | sort); do
activity=$(tmux display-message -t "$session" -p '#{session_activity}' 2>/dev/null)
now=$(date +%s)
if [ -n "$activity" ]; then
diff=$((now - activity))
if [ $diff -lt 60 ]; then
last_active="${diff}s ago"
elif [ $diff -lt 3600 ]; then
last_active="$((diff / 60))m ago"
elif [ $diff -lt 86400 ]; then
last_active="$((diff / 3600))h ago"
else
last_active="$((diff / 86400))d ago"
fi
else
last_active="-"
fi
data_file="$HOME/.splitly-sessions/$session"
meta_branch=$(grep "^branch=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
meta_status=$(grep "^status=" "$data_file" 2>/dev/null | cut -d'=' -f2-)
meta_summary=$(grep "^summary=" "$data_file" 2>/dev/null | cut -d'=' -f2- | cut -c1-60)
# Get live Claude session info
eval "$(get_claude_info "$session" 2>/dev/null)"
echo " 📍 $session ($last_active)"
# Show branch - prefer live, show if different from metadata
if [ -n "$claude_branch" ]; then
if [ -n "$meta_branch" ] && [ "$claude_branch" != "$meta_branch" ]; then
echo " Branch: $claude_branch (meta: $meta_branch)"
else
echo " Branch: $claude_branch"
fi
elif [ -n "$meta_branch" ]; then
echo " Branch: $meta_branch (stale)"
fi
# Show summary - prefer Claude's auto-generated, fall back to metadata
if [ -n "$claude_summary" ]; then
echo " Claude: ${claude_summary:0:65}"
fi
if [ -n "$meta_summary" ]; then
echo " Status: ${meta_summary}..."
fi
# Show session ID if available (useful for resuming)
[ -n "$claude_session_id" ] && echo " Session: ${claude_session_id:0:8}..."
done
fi
echo ""
echo "┌──────────────────────────────────────────────────────────────────────────────┐"
echo "│ QUICK ACTIONS │"
echo "└──────────────────────────────────────────────────────────────────────────────┘"
echo " ~/claude-spawn i [linear-url] - New integrator session in new iTerm2 tab"
echo " ~/claude-spawn s [issue-num] - New splitly session in new iTerm2 tab"
echo " ~/claude-integrator-session cleanup"
echo " ~/claude-splitly-session cleanup"
echo ""

63
scripts/get-claude-session-info Executable file
View File

@ -0,0 +1,63 @@
#!/bin/bash
# Get Claude session info for a tmux session
# Extracts session ID, branch, and summary directly from Claude's data
TMUX_SESSION="$1"
if [ -z "$TMUX_SESSION" ]; then
echo "Usage: $0 <tmux-session-name>"
exit 1
fi
# 1. Get the TTY for this tmux session
TTY=$(tmux list-panes -t "$TMUX_SESSION" -F '#{pane_tty}' 2>/dev/null | head -1)
if [ -z "$TTY" ]; then
echo "error: tmux session '$TMUX_SESSION' not found"
exit 1
fi
# 2. Find the Claude PID running on that TTY
TTY_SHORT="${TTY#/dev/}"
CLAUDE_PID=$(ps -eo pid,tty,comm 2>/dev/null | awk -v tty="$TTY_SHORT" '$2 == tty && $3 == "claude" {print $1; exit}')
if [ -z "$CLAUDE_PID" ]; then
echo "error: no claude process on $TTY"
exit 1
fi
# 3. Get the working directory of that process
CWD=$(lsof -p "$CLAUDE_PID" 2>/dev/null | awk '$4 == "cwd" {print $NF}')
# 4. Get current git branch from that directory
BRANCH=$(git -C "$CWD" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
# 5. Convert to Claude's project path format and find active session
PROJECT_PATH=$(echo "$CWD" | tr '/.' '--')
PROJECT_DIR="$HOME/.claude/projects/$PROJECT_PATH"
if [ ! -d "$PROJECT_DIR" ]; then
echo "session_id: unknown"
echo "branch: $BRANCH"
echo "summary: (no claude project found)"
exit 0
fi
# 6. Find the most recently modified session file
ACTIVE_SESSION_FILE=$(ls -t "$PROJECT_DIR"/*.jsonl 2>/dev/null | grep -v "agent-" | head -1)
if [ -z "$ACTIVE_SESSION_FILE" ]; then
echo "session_id: unknown"
echo "branch: $BRANCH"
echo "summary: (no session file)"
exit 0
fi
SESSION_ID=$(basename "$ACTIVE_SESSION_FILE" .jsonl)
# 7. Extract the most recent summary from the session file
SUMMARY=$(grep '"type":"summary"' "$ACTIVE_SESSION_FILE" 2>/dev/null | tail -1 | jq -r '.summary // empty')
if [ -z "$SUMMARY" ]; then
# Fall back to first user message
SUMMARY=$(grep '"type":"user"' "$ACTIVE_SESSION_FILE" 2>/dev/null | head -1 | jq -r '.message.content' 2>/dev/null | head -c 80)
fi
echo "session_id: $SESSION_ID"
echo "branch: $BRANCH"
echo "summary: ${SUMMARY:-"(no summary)"}"

30
scripts/notify-session Executable file
View File

@ -0,0 +1,30 @@
#!/bin/bash
# Usage: ~/notify-session [message]
#
# Sends an iTerm2 notification that navigates to THIS tab when clicked.
# Works when called from Claude Code's Bash tool (which captures stdout).
#
# Examples:
# ~/notify-session "PR comments addressed, ready for review"
# ~/notify-session "Build complete"
# ~/notify-session # defaults to "Task complete"
#
# Writes directly to the tmux pane's TTY so the escape sequence reaches
# iTerm2 even when stdout is captured by Claude's Bash tool.
MESSAGE="${1:-Task complete}"
SESSION_NAME=$(tmux display-message -p '#S' 2>/dev/null || echo "Claude")
# Get the TTY of the current tmux pane
PANE_TTY=$(tmux display-message -p '#{pane_tty}' 2>/dev/null)
if [ -n "$PANE_TTY" ]; then
# Write directly to the pane's TTY with tmux passthrough
printf '\033Ptmux;\033\033]9;%s\007\033\\' "$MESSAGE" > "$PANE_TTY"
elif [ -n "$TMUX" ]; then
# Fallback: we're in tmux but can't find pane TTY
printf '\033Ptmux;\033\033]9;%s\007\033\\' "$MESSAGE"
else
# Outside tmux: send OSC 9 directly
printf '\033]9;%s\007' "$MESSAGE"
fi

58
scripts/open-iterm-tab Executable file
View File

@ -0,0 +1,58 @@
#!/bin/bash
# Usage: ~/open-iterm-tab <session-name>
# Opens a tmux session in iTerm2 - reuses existing tab if found, otherwise opens new
# Sets both the profile name (for detection) and tab title (for visual display)
SESSION="$1"
if [ -z "$SESSION" ]; then
echo "Usage: ~/open-iterm-tab <session-name>"
exit 1
fi
# Verify tmux session exists
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Error: tmux session '$SESSION' does not exist"
exit 1
fi
# Check if there's already an iTerm2 tab with this session's profile name
result=$(osascript -e "
tell application \"iTerm2\"
activate
repeat with aWindow in windows
repeat with aTab in tabs of aWindow
repeat with aSession in sessions of aTab
try
if profile name of aSession is equal to \"$SESSION\" then
select aWindow
select aTab
return \"EXISTING\"
end if
end try
end repeat
end repeat
end repeat
return \"NOT_FOUND\"
end tell
" 2>&1)
if [ "$result" = "EXISTING" ]; then
echo "✓ Switched to existing tab for $SESSION"
else
echo "Opening new tab for $SESSION..."
osascript -e "
tell application \"iTerm2\"
activate
tell current window
create tab with default profile
tell current session
-- Set profile name for programmatic detection
set name to \"$SESSION\"
-- Set visible tab title via escape code, then attach to tmux
write text \"printf '\\\\033]0;$SESSION\\\\007' && tmux attach -t $SESSION\"
end tell
end tell
end tell
" 2>&1
echo "✓ Opened $SESSION in new iTerm2 tab"
fi

53
scripts/open-tmux-session Executable file
View File

@ -0,0 +1,53 @@
#!/bin/bash
# Switch to the iTerm tab running a given tmux session
# Usage: ~/open-tmux-session <session-name>
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
SESSION="$1"
[ -z "$SESSION" ] && echo "Usage: open-tmux-session <session-name>" && exit 1
# Find the tty attached to this tmux session
TARGET_TTY=$(tmux list-clients -t "$SESSION" -F "#{client_tty}" 2>/dev/null | head -1)
if [ -n "$TARGET_TTY" ]; then
# Find and switch to the iTerm tab with this tty
osascript << APPLESCRIPT
tell application "iTerm2"
activate
set foundIt to false
repeat with w in windows
tell w
set tabCount to count of tabs
repeat with i from 1 to tabCount
set theTab to item i of tabs
repeat with theSession in sessions of theTab
set sessionTTY to tty of theSession
if sessionTTY is "$TARGET_TTY" then
set index of w to 1
select theTab
set foundIt to true
exit repeat
end if
end repeat
if foundIt then exit repeat
end repeat
end tell
if foundIt then exit repeat
end repeat
end tell
APPLESCRIPT
else
# No client attached — create a new iTerm tab and attach
osascript << APPLESCRIPT
tell application "iTerm2"
activate
tell current window
set newTab to (create tab with default profile)
tell current session of newTab
write text "tmux attach -t $SESSION"
end tell
end tell
end tell
APPLESCRIPT
fi

112
scripts/send-to-session Executable file
View File

@ -0,0 +1,112 @@
#!/bin/bash
# Usage: ~/send-to-session <session-name> <message>
# or: ~/send-to-session <session-name> -f <file>
# Sends a message to a Claude agent in a tmux session and submits it.
#
# If the session is busy (Claude is thinking), waits for it to become idle
# before sending. Retries Enter if the message wasn't picked up.
SESSION="$1"
shift
if [ -z "$SESSION" ]; then
echo "Usage: ~/send-to-session <session-name> <message>"
echo " or: ~/send-to-session <session-name> -f <file>"
exit 1
fi
# Verify session exists
if ! tmux has-session -t "$SESSION" 2>/dev/null; then
echo "Error: Session '$SESSION' does not exist"
exit 1
fi
is_busy() {
# Deterministic: check if the last non-empty line has a prompt () or shell ($) — means idle
local last_line=$(tmux capture-pane -t "$SESSION" -p -S -5 2>/dev/null | grep -v '^$' | tail -1)
# Idle indicators: prompt char or permission mode line
if echo "$last_line" | grep -qE '|^\$|⏵⏵|bypass permissions'; then
return 1 # not busy
fi
# Active indicators: "esc to interrupt" only appears during live processing
local output=$(tmux capture-pane -t "$SESSION" -p -S -3 2>/dev/null)
echo "$output" | grep -qF "esc to interrupt"
}
is_processing() {
local output=$(tmux capture-pane -t "$SESSION" -p -S -10 2>/dev/null)
echo "$output" | grep -qE "Thinking|Roosting|Garnishing|Levitating|Baking|Whirring|Musing|Cooked|Churned|Brewed|Worked|Cogitated|Frolicking|Prestidigitating|Whisking|Mustering|Percolating|Running…|esc to interrupt|⏺"
}
has_queued_message() {
local output=$(tmux capture-pane -t "$SESSION" -p -S -5 2>/dev/null)
echo "$output" | grep -qE "Press up to edit queued messages"
}
# Wait for session to be idle (up to 10 minutes)
WAIT_COUNT=0
MAX_WAIT=120 # 120 * 5s = 10 minutes
while is_busy; do
if [ $WAIT_COUNT -eq 0 ]; then
echo "⏳ Waiting for $SESSION to finish current task..."
fi
WAIT_COUNT=$((WAIT_COUNT + 1))
if [ $WAIT_COUNT -ge $MAX_WAIT ]; then
echo "⚠ Timeout waiting for $SESSION to become idle. Sending anyway."
break
fi
sleep 5
done
# Clear any partial input
tmux send-keys -t "$SESSION" C-u
sleep 0.2
# Send the message
send_message() {
if [ "$1" = "-f" ]; then
local file="$2"
if [ ! -f "$file" ]; then
echo "Error: File not found: $file"
exit 1
fi
tmux load-buffer "$file"
tmux paste-buffer -t "$SESSION"
else
local msg="$*"
if echo "$msg" | grep -q $'\n' || [ ${#msg} -gt 200 ]; then
local tmpfile=$(mktemp /tmp/send-to-session-XXXXXX.txt)
printf '%s' "$msg" > "$tmpfile"
tmux load-buffer "$tmpfile"
tmux paste-buffer -t "$SESSION"
rm -f "$tmpfile"
else
tmux send-keys -t "$SESSION" "$msg"
fi
fi
}
send_message "$@"
sleep 0.3
tmux send-keys -t "$SESSION" Enter
# Verify it's processing, retry Enter if needed
for attempt in 1 2 3; do
sleep 2
if is_processing; then
echo "✓ Message sent and processing"
exit 0
fi
# Check if message is sitting in queue (Claude was still wrapping up)
if has_queued_message; then
echo "✓ Message queued (session is finishing previous task)"
exit 0
fi
# Message might be in input buffer — retry Enter
if [ $attempt -lt 3 ]; then
tmux send-keys -t "$SESSION" Enter
sleep 1
fi
done
echo "⚠ Message sent - could not confirm it was received"