diff --git a/.gitignore b/.gitignore
index 4a59924..f61dc98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,12 @@ logs/user_prompt_submit.json
# Bun build artifacts
*.bun-build
claude-code/hooks/*.bun-build
-.code/
\ No newline at end of file
+.code/
+# Orchestration runtime state (DO NOT COMMIT)
+.claude/orchestration/state/sessions.json
+.claude/orchestration/state/completed.json
+.claude/orchestration/state/dag-*.json
+.claude/orchestration/checkpoints/*.json
+
+# Keep config template
+!.claude/orchestration/state/config.json
diff --git a/claude-code-4.5/CLAUDE.md b/claude-code-4.5/CLAUDE.md
index e0fd469..c7ceda7 100644
--- a/claude-code-4.5/CLAUDE.md
+++ b/claude-code-4.5/CLAUDE.md
@@ -177,11 +177,12 @@ async def process_payment(amount: int, customer_id: str):
# Background Process Management
-CRITICAL: When starting any long-running server process (web servers, development servers, APIs, etc.), you MUST:
+CRITICAL: When starting any long-running server process (web servers, development servers, APIs, etc.), you MUST use tmux for persistence and management:
-1. **Always Run in Background**
+1. **Always Run in tmux Sessions**
- NEVER run servers in foreground as this will block the agent process indefinitely
- - Use background execution (`&` or `nohup`) or container-use background mode
+ - ALWAYS use tmux for background execution (provides persistence across disconnects)
+ - Fallback to container-use background mode if tmux unavailable
- Examples of foreground-blocking commands:
- `npm run dev` or `npm start`
- `python app.py` or `flask run`
@@ -193,96 +194,164 @@ CRITICAL: When starting any long-running server process (web servers, developmen
- ALWAYS use random/dynamic ports to avoid conflicts between parallel sessions
- Generate random port: `PORT=$(shuf -i 3000-9999 -n 1)`
- Pass port via environment variable or command line argument
- - Document the assigned port in logs for reference
+ - Document the assigned port in session metadata
-3. **Mandatory Log Redirection**
- - Redirect all output to log files: `command > app.log 2>&1 &`
- - Use descriptive log names: `server.log`, `api.log`, `dev-server.log`
- - Include port in log name when possible: `server-${PORT}.log`
- - Capture both stdout and stderr for complete debugging information
+3. **tmux Session Naming Convention**
+ - Dev environments: `dev-{project}-{timestamp}`
+ - Spawned agents: `agent-{timestamp}`
+ - Monitoring: `monitor-{purpose}`
+ - Examples: `dev-myapp-1705161234`, `agent-1705161234`
-4. **Container-use Background Mode**
- - When using container-use, ALWAYS set `background: true` for server commands
- - Use `ports` parameter to expose the randomly assigned port
- - Example: `mcp__container-use__environment_run_cmd` with `background: true, ports: [PORT]`
+4. **Session Metadata**
+ - Save session info to `.tmux-dev-session.json` (per project)
+ - Include: session name, ports, services, created timestamp
+ - Use metadata for session discovery and conflict detection
-5. **Log Monitoring**
- - After starting background process, immediately check logs with `tail -f logfile.log`
- - Use `cat logfile.log` to view full log contents
- - Monitor startup messages to ensure server started successfully
- - Look for port assignment confirmation in logs
+5. **Log Capture**
+ - Use `| tee logfile.log` to capture output to both tmux and file
+ - Use descriptive log names: `server.log`, `api.log`, `dev-server.log`
+ - Include port in log name when possible: `server-${PORT}.log`
+ - Logs visible in tmux pane AND saved to disk
6. **Safe Process Management**
- - NEVER kill by process name (`pkill node`, `pkill vite`, `pkill uv`) - this affects other parallel sessions
+ - NEVER kill by process name (`pkill node`, `pkill vite`, `pkill uv`) - affects other sessions
- ALWAYS kill by port to target specific server: `lsof -ti:${PORT} | xargs kill -9`
- - Alternative port-based killing: `fuser -k ${PORT}/tcp`
- - Check what's running on port before killing: `lsof -i :${PORT}`
- - Clean up port-specific processes before starting new servers on same port
+ - Alternative: Kill entire tmux session: `tmux kill-session -t {session-name}`
+ - Check what's running on port: `lsof -i :${PORT}`
**Examples:**
```bash
-# ❌ WRONG - Will block forever and use default port
+# ❌ WRONG - Will block forever
npm run dev
# ❌ WRONG - Killing by process name affects other sessions
pkill node
-# ✅ CORRECT - Complete workflow with random port
+# ❌ DEPRECATED - Using & background jobs (no persistence)
PORT=$(shuf -i 3000-9999 -n 1)
-echo "Starting server on port $PORT"
PORT=$PORT npm run dev > dev-server-${PORT}.log 2>&1 &
-tail -f dev-server-${PORT}.log
+
+# ✅ CORRECT - Complete tmux workflow with random port
+PORT=$(shuf -i 3000-9999 -n 1)
+SESSION="dev-$(basename $(pwd))-$(date +%s)"
+
+# Create tmux session
+tmux new-session -d -s "$SESSION" -n dev-server
+
+# Start server in tmux with log capture
+tmux send-keys -t "$SESSION:dev-server" "PORT=$PORT npm run dev | tee dev-server-${PORT}.log" C-m
+
+# Save metadata
+cat > .tmux-dev-session.json </dev/null && echo "Session running"
-# ✅ CORRECT - Container-use with random port
+# ✅ CORRECT - Attach to monitor logs
+tmux attach -t "$SESSION"
+
+# ✅ CORRECT - Flask/Python in tmux
+PORT=$(shuf -i 5000-5999 -n 1)
+SESSION="dev-flask-$(date +%s)"
+tmux new-session -d -s "$SESSION" -n server
+tmux send-keys -t "$SESSION:server" "FLASK_RUN_PORT=$PORT flask run | tee flask-${PORT}.log" C-m
+
+# ✅ CORRECT - Next.js in tmux
+PORT=$(shuf -i 3000-3999 -n 1)
+SESSION="dev-nextjs-$(date +%s)"
+tmux new-session -d -s "$SESSION" -n server
+tmux send-keys -t "$SESSION:server" "PORT=$PORT npm run dev | tee nextjs-${PORT}.log" C-m
+```
+
+**Fallback: Container-use Background Mode** (when tmux unavailable):
+```bash
+# Only use if tmux is not available
mcp__container-use__environment_run_cmd with:
command: "PORT=${PORT} npm run dev"
background: true
ports: [PORT]
-
-# ✅ CORRECT - Flask/Python example
-PORT=$(shuf -i 3000-9999 -n 1)
-FLASK_RUN_PORT=$PORT python app.py > flask-${PORT}.log 2>&1 &
-
-# ✅ CORRECT - Next.js example
-PORT=$(shuf -i 3000-9999 -n 1)
-PORT=$PORT npm run dev > nextjs-${PORT}.log 2>&1 &
```
-**Playwright Testing Background Execution:**
+**Playwright Testing in tmux:**
-- **ALWAYS run Playwright tests in background** to prevent agent blocking
-- **NEVER open test report servers** - they will block agent execution indefinitely
-- Use `--reporter=json` and `--reporter=line` for programmatic result parsing
-- Redirect all output to log files for later analysis
+- **Run Playwright tests in tmux** for persistence and log monitoring
+- **NEVER open test report servers** - they block agent execution
+- Use `--reporter=json` and `--reporter=line` for programmatic parsing
- Examples:
```bash
-# ✅ CORRECT - Background Playwright execution
-npx playwright test --reporter=json > playwright-results.log 2>&1 &
+# ✅ CORRECT - Playwright in tmux session
+SESSION="test-playwright-$(date +%s)"
+tmux new-session -d -s "$SESSION" -n tests
+tmux send-keys -t "$SESSION:tests" "npx playwright test --reporter=json | tee playwright-results.log" C-m
-# ✅ CORRECT - Custom config with background execution
-npx playwright test --config=custom.config.js --reporter=line > test-output.log 2>&1 &
+# Monitor progress
+tmux attach -t "$SESSION"
+
+# ❌ DEPRECATED - Background job (no persistence)
+npx playwright test --reporter=json > playwright-results.log 2>&1 &
# ❌ WRONG - Will block agent indefinitely
npx playwright test --reporter=html
npx playwright show-report
# ✅ CORRECT - Parse results programmatically
-cat playwright-results.json | jq '.stats'
-tail -20 test-output.log
+cat playwright-results.log | jq '.stats'
```
+**Using Generic /start-* Commands:**
+
+For common development scenarios, use the generic commands:
+
+```bash
+# Start local web development (auto-detects framework)
+/start-local development # Uses .env.development
+/start-local staging # Uses .env.staging
+/start-local production # Uses .env.production
+
+# Start iOS development (auto-detects project type)
+/start-ios Debug # Uses .env.development
+/start-ios Staging # Uses .env.staging
+/start-ios Release # Uses .env.production
+
+# Start Android development (auto-detects project type)
+/start-android debug # Uses .env.development
+/start-android staging # Uses .env.staging
+/start-android release # Uses .env.production
+```
-RATIONALE: Background execution with random ports prevents agent process deadlock while enabling parallel sessions to coexist without interference. Port-based process management ensures safe cleanup without affecting other concurrent development sessions. This maintains full visibility into server status through logs while ensuring continuous agent operation.
+These commands automatically:
+- Create organized tmux sessions
+- Assign random ports
+- Start all required services
+- Save session metadata
+- Setup log monitoring
+
+**Session Persistence Benefits:**
+- Survives SSH disconnects
+- Survives terminal restarts
+- Easy reattachment: `tmux attach -t {session-name}`
+- Live log monitoring in split panes
+- Organized multi-window layouts
+
+RATIONALE: tmux provides persistence across disconnects, better visibility through split panes, and session organization. Random ports prevent conflicts between parallel sessions. Port-based or session-based process management ensures safe cleanup. Generic /start-* commands provide consistent, framework-agnostic development environments.
# Session Management System
diff --git a/claude-code-4.5/commands/attach-agent-worktree.md b/claude-code-4.5/commands/attach-agent-worktree.md
new file mode 100644
index 0000000..a141096
--- /dev/null
+++ b/claude-code-4.5/commands/attach-agent-worktree.md
@@ -0,0 +1,46 @@
+# /attach-agent-worktree - Attach to Agent Session
+
+Changes to agent worktree directory and attaches to its tmux session.
+
+## Usage
+
+```bash
+/attach-agent-worktree {timestamp}
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+AGENT_ID="$1"
+
+if [ -z "$AGENT_ID" ]; then
+ echo "❌ Agent ID required"
+ echo "Usage: /attach-agent-worktree {timestamp}"
+ exit 1
+fi
+
+# Find worktree directory
+WORKTREE_DIR=$(find worktrees -type d -name "agent-${AGENT_ID}*" 2>/dev/null | head -1)
+
+if [ -z "$WORKTREE_DIR" ] || [ ! -d "$WORKTREE_DIR" ]; then
+ echo "❌ Worktree not found for agent: $AGENT_ID"
+ exit 1
+fi
+
+SESSION="agent-${AGENT_ID}"
+
+# Check if tmux session exists
+if ! tmux has-session -t "$SESSION" 2>/dev/null; then
+ echo "❌ Tmux session not found: $SESSION"
+ exit 1
+fi
+
+echo "📂 Worktree: $WORKTREE_DIR"
+echo "🔗 Attaching to session: $SESSION"
+echo ""
+
+# Attach to session
+tmux attach -t "$SESSION"
+```
diff --git a/claude-code-4.5/commands/cleanup-agent-worktree.md b/claude-code-4.5/commands/cleanup-agent-worktree.md
new file mode 100644
index 0000000..12f7ebb
--- /dev/null
+++ b/claude-code-4.5/commands/cleanup-agent-worktree.md
@@ -0,0 +1,36 @@
+# /cleanup-agent-worktree - Remove Agent Worktree
+
+Removes a specific agent worktree and its branch.
+
+## Usage
+
+```bash
+/cleanup-agent-worktree {timestamp}
+/cleanup-agent-worktree {timestamp} --force
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+AGENT_ID="$1"
+FORCE="$2"
+
+if [ -z "$AGENT_ID" ]; then
+ echo "❌ Agent ID required"
+ echo "Usage: /cleanup-agent-worktree {timestamp} [--force]"
+ exit 1
+fi
+
+# Source utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/../utils/git-worktree-utils.sh"
+
+# Cleanup worktree
+if [ "$FORCE" = "--force" ]; then
+ cleanup_agent_worktree "$AGENT_ID" true
+else
+ cleanup_agent_worktree "$AGENT_ID" false
+fi
+```
diff --git a/claude-code-4.5/commands/handover.md b/claude-code-4.5/commands/handover.md
index e57f076..36a9e37 100644
--- a/claude-code-4.5/commands/handover.md
+++ b/claude-code-4.5/commands/handover.md
@@ -1,59 +1,187 @@
-# Handover Command
+# /handover - Generate Session Handover Document
-Use this command to generate a session handover document when transferring work to another team member or continuing work in a new session.
+Generate a handover document for transferring work to another developer or spawning an async agent.
## Usage
-```
-/handover [optional-notes]
+```bash
+/handover # Standard handover
+/handover "notes about current work" # With notes
+/handover --agent-spawn "task desc" # For spawning agent
```
-## Description
+## Modes
-This command generates a comprehensive handover document that includes:
+### Standard Handover (default)
-- Current session health status
+For transferring work to another human or resuming later:
+- Current session health
- Task progress and todos
-- Technical context and working files
-- Instructions for resuming work
-- Any blockers or important notes
+- Technical context
+- Resumption instructions
-## Example
+### Agent Spawn Mode (`--agent-spawn`)
+For passing context to spawned agents:
+- Focused on task context
+- Technical stack details
+- Success criteria
+- Files to modify
+
+## Implementation
+
+### Detect Mode
+
+```bash
+MODE="standard"
+AGENT_TASK=""
+NOTES="${1:-}"
+
+if [[ "$1" == "--agent-spawn" ]]; then
+ MODE="agent"
+ AGENT_TASK="${2:-}"
+ shift 2
+fi
```
-/handover Working on authentication refactor, need to complete OAuth integration
+
+### Generate Timestamp
+
+```bash
+TIMESTAMP=$(date +"%Y-%m-%d-%H-%M-%S")
+DISPLAY_TIME=$(date +"%Y-%m-%d %H:%M:%S")
+FILENAME="handover-${TIMESTAMP}.md"
+PRIMARY_LOCATION="${TOOL_DIR}/session/${FILENAME}"
+BACKUP_LOCATION="./${FILENAME}"
+
+mkdir -p "${TOOL_DIR}/session"
```
-## Output Location
+### Standard Handover Content
+
+```markdown
+# Handover Document
+
+**Generated**: ${DISPLAY_TIME}
+**Session**: $(tmux display-message -p '#S' 2>/dev/null || echo 'unknown')
+
+## Current Work
+
+[Describe what you're working on]
+
+## Task Progress
+
+[List todos and completion status]
+
+## Technical Context
+
+**Current Branch**: $(git branch --show-current)
+**Last Commit**: $(git log -1 --oneline)
+**Modified Files**:
+$(git status --short)
+
+## Resumption Instructions
+
+1. Review changes: git diff
+2. Continue work on [specific task]
+3. Test with: [test command]
+
+## Notes
+
+${NOTES}
+```
+
+### Agent Spawn Handover Content
+
+```markdown
+# Agent Handover - ${AGENT_TASK}
+
+**Generated**: ${DISPLAY_TIME}
+**Parent Session**: $(tmux display-message -p '#S' 2>/dev/null || echo 'unknown')
+**Agent Task**: ${AGENT_TASK}
+
+## Context Summary
+
+**Current Work**: [What's in progress]
+**Current Branch**: $(git branch --show-current)
+**Last Commit**: $(git log -1 --oneline)
+
+## Task Details
+
+**Agent Mission**: ${AGENT_TASK}
+
+**Requirements**:
+- [List specific requirements]
+- [What needs to be done]
+
+**Success Criteria**:
+- [How to know when done]
+
+## Technical Context
+
+**Stack**: [Technology stack]
+**Key Files**:
+$(git status --short)
+
+**Modified Recently**:
+$(git log --name-only -5 --oneline)
-The handover document MUST be saved to:
-- **Primary Location**: `.{{TOOL_DIR}}/session/handover-{{TIMESTAMP}}.md`
-- **Backup Location**: `./handover-{{TIMESTAMP}}.md` (project root)
+## Instructions for Agent
-## File Naming Convention
+1. Review current implementation
+2. Make specified changes
+3. Add/update tests
+4. Verify all tests pass
+5. Commit with clear message
-Use this format: `handover-YYYY-MM-DD-HH-MM-SS.md`
+## References
-Example: `handover-2024-01-15-14-30-45.md`
+**Documentation**: [Links to relevant docs]
+**Related Work**: [Related PRs/issues]
+```
+
+### Save Document
-**CRITICAL**: Always obtain the timestamp programmatically:
```bash
-# Generate timestamp - NEVER type dates manually
-TIMESTAMP=$(date +"%Y-%m-%d-%H-%M-%S")
-FILENAME="handover-${TIMESTAMP}.md"
+# Generate appropriate content based on MODE
+if [ "$MODE" = "agent" ]; then
+ # Generate agent handover content
+ CONTENT="[Agent handover content from above]"
+else
+ # Generate standard handover content
+ CONTENT="[Standard handover content from above]"
+fi
+
+# Save to primary location
+echo "$CONTENT" > "$PRIMARY_LOCATION"
+
+# Save backup
+echo "$CONTENT" > "$BACKUP_LOCATION"
+
+echo "✅ Handover document generated"
+echo ""
+echo "Primary: $PRIMARY_LOCATION"
+echo "Backup: $BACKUP_LOCATION"
+echo ""
```
-## Implementation
+## Output Location
+
+**Primary**: `${TOOL_DIR}/session/handover-{timestamp}.md`
+**Backup**: `./handover-{timestamp}.md`
+
+## Integration with spawn-agent
+
+The `/spawn-agent` command automatically calls `/handover --agent-spawn` when `--with-handover` flag is used:
+
+```bash
+/spawn-agent codex "refactor auth" --with-handover
+# Internally calls: /handover --agent-spawn "refactor auth"
+# Copies handover to agent worktree as .agent-handover.md
+```
+
+## Notes
-1. **ALWAYS** get the current timestamp using `date` command:
- ```bash
- date +"%Y-%m-%d %H:%M:%S" # For document header
- date +"%Y-%m-%d-%H-%M-%S" # For filename
- ```
-2. Generate handover using `{{HOME_TOOL_DIR}}/templates/handover-template.md`
-3. Replace all `{{VARIABLE}}` placeholders with actual values
-4. Save to BOTH locations (primary and backup)
-5. Display the full file path to the user for reference
-6. Verify the date in the filename matches the date in the document header
-
-The handover document will be saved as a markdown file and can be used to seamlessly continue work in a new session.
\ No newline at end of file
+- Always uses programmatic timestamps (never manual)
+- Saves to both primary and backup locations
+- Agent mode focuses on task context, not session health
+- Standard mode includes full session state
diff --git a/claude-code-4.5/commands/list-agent-worktrees.md b/claude-code-4.5/commands/list-agent-worktrees.md
new file mode 100644
index 0000000..3534902
--- /dev/null
+++ b/claude-code-4.5/commands/list-agent-worktrees.md
@@ -0,0 +1,16 @@
+# /list-agent-worktrees - List All Agent Worktrees
+
+Shows all active agent worktrees with their paths and branches.
+
+## Usage
+
+```bash
+/list-agent-worktrees
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+git worktree list | grep "worktrees/agent-" || echo "No agent worktrees found"
+```
diff --git a/claude-code-4.5/commands/m-implement.md b/claude-code-4.5/commands/m-implement.md
new file mode 100644
index 0000000..b713e44
--- /dev/null
+++ b/claude-code-4.5/commands/m-implement.md
@@ -0,0 +1,375 @@
+---
+description: Multi-agent implementation - Execute DAG in waves with automated monitoring
+tags: [orchestration, implementation, multi-agent]
+---
+
+# Multi-Agent Implementation (`/m-implement`)
+
+You are now in **multi-agent implementation mode**. Your task is to execute a pre-planned DAG by spawning agents in waves and monitoring their progress.
+
+## Your Role
+
+Act as an **orchestrator** that manages parallel agent execution, monitors progress, and handles failures.
+
+## Prerequisites
+
+1. **DAG file must exist**: `~/.claude/orchestration/state/dag-.json`
+2. **Session must be created**: Via `/m-plan` or manually
+3. **Git worktrees setup**: Project must support git worktrees
+
+## Process
+
+### Step 1: Load DAG and Session
+
+```bash
+# Load DAG file
+DAG_FILE="~/.claude/orchestration/state/dag-${SESSION_ID}.json"
+
+# Verify DAG exists
+if [ ! -f "$DAG_FILE" ]; then
+ echo "Error: DAG file not found: $DAG_FILE"
+ exit 1
+fi
+
+# Load session
+SESSION=$(~/.claude/utils/orchestrator-state.sh get "$SESSION_ID")
+
+if [ -z "$SESSION" ]; then
+ echo "Error: Session not found: $SESSION_ID"
+ exit 1
+fi
+```
+
+### Step 2: Calculate Waves
+
+```bash
+# Get waves from DAG (already calculated in /m-plan)
+WAVES=$(jq -r '.waves[] | "\(.wave_number):\(.nodes | join(" "))"' "$DAG_FILE")
+
+# Example output:
+# 1:ws-1 ws-3
+# 2:ws-2 ws-4
+# 3:ws-5
+```
+
+### Step 3: Execute Wave-by-Wave
+
+**For each wave:**
+
+```bash
+WAVE_NUMBER=1
+
+# Get nodes in this wave
+WAVE_NODES=$(echo "$WAVES" | grep "^${WAVE_NUMBER}:" | cut -d: -f2)
+
+echo "🌊 Starting Wave $WAVE_NUMBER: $WAVE_NODES"
+
+# Update wave status
+~/.claude/utils/orchestrator-state.sh update-wave-status "$SESSION_ID" "$WAVE_NUMBER" "active"
+
+# Spawn all agents in wave (parallel)
+for node in $WAVE_NODES; do
+ spawn_agent "$SESSION_ID" "$node" &
+done
+
+# Wait for all agents in wave to complete
+wait
+
+# Check if wave completed successfully
+if wave_all_complete "$SESSION_ID" "$WAVE_NUMBER"; then
+ ~/.claude/utils/orchestrator-state.sh update-wave-status "$SESSION_ID" "$WAVE_NUMBER" "complete"
+ echo "✅ Wave $WAVE_NUMBER complete"
+else
+ echo "❌ Wave $WAVE_NUMBER failed"
+ exit 1
+fi
+```
+
+### Step 4: Spawn Agent Function
+
+**Function to spawn a single agent:**
+
+```bash
+spawn_agent() {
+ local session_id="$1"
+ local node_id="$2"
+
+ # Get node details from DAG
+ local node=$(jq -r --arg n "$node_id" '.nodes[$n]' "$DAG_FILE")
+ local task=$(echo "$node" | jq -r '.task')
+ local agent_type=$(echo "$node" | jq -r '.agent_type')
+ local workstream_id=$(echo "$node" | jq -r '.workstream_id')
+
+ # Create git worktree
+ local worktree_dir="worktrees/${workstream_id}-${node_id}"
+ local branch="feat/${workstream_id}"
+
+ git worktree add "$worktree_dir" -b "$branch" 2>/dev/null || git worktree add "$worktree_dir" "$branch"
+
+ # Create tmux session
+ local agent_id="agent-${workstream_id}-$(date +%s)"
+ tmux new-session -d -s "$agent_id" -c "$worktree_dir"
+
+ # Start Claude in tmux
+ tmux send-keys -t "$agent_id" "claude --dangerously-skip-permissions" C-m
+
+ # Wait for Claude to initialize
+ wait_for_claude_ready "$agent_id"
+
+ # Send task
+ local full_task="$task
+
+AGENT ROLE: Act as a ${agent_type}.
+
+CRITICAL REQUIREMENTS:
+- Work in worktree: $worktree_dir
+- Branch: $branch
+- When complete: Run tests, commit with clear message, report status
+
+DELIVERABLES:
+$(echo "$node" | jq -r '.deliverables[]' | sed 's/^/- /')
+
+When complete: Commit all changes and report status."
+
+ tmux send-keys -t "$agent_id" -l "$full_task"
+ tmux send-keys -t "$agent_id" C-m
+
+ # Add agent to session state
+ local agent_config=$(cat <15min, killing..."
+ ~/.claude/utils/orchestrator-agent.sh kill "$tmux_session"
+ ~/.claude/utils/orchestrator-state.sh update-agent-status "$session_id" "$agent_id" "killed"
+ fi
+ done
+
+ # Check if wave is complete
+ if wave_all_complete "$session_id" "$wave_number"; then
+ return 0
+ fi
+
+ # Check if wave failed
+ local failed_count=$(~/.claude/utils/orchestrator-state.sh list-agents "$session_id" | \
+ xargs -I {} ~/.claude/utils/orchestrator-state.sh get-agent "$session_id" {} | \
+ jq -r 'select(.status == "failed")' | wc -l)
+
+ if [ "$failed_count" -gt 0 ]; then
+ echo "❌ Wave $wave_number failed ($failed_count agents failed)"
+ return 1
+ fi
+
+ # Sleep before next check
+ sleep 30
+ done
+}
+```
+
+### Step 7: Handle Completion
+
+**When all waves complete:**
+
+```bash
+# Archive session
+~/.claude/utils/orchestrator-state.sh archive "$SESSION_ID"
+
+# Print summary
+echo "🎉 All waves complete!"
+echo ""
+echo "Summary:"
+echo " Total Cost: \$$(jq -r '.total_cost_usd' sessions.json)"
+echo " Total Agents: $(jq -r '.agents | length' sessions.json)"
+echo " Duration: "
+echo ""
+echo "Next steps:"
+echo " 1. Review agent outputs in worktrees"
+echo " 2. Merge worktrees to main branch"
+echo " 3. Run integration tests"
+```
+
+## Output Format
+
+**During execution, display:**
+
+```
+🚀 Multi-Agent Implementation:
+
+📊 Plan Summary:
+ - Total Workstreams: 7
+ - Total Waves: 4
+ - Max Concurrent: 4
+
+🌊 Wave 1 (2 agents)
+ ✅ agent-ws1-xxx (complete) - Cost: $1.86
+ ✅ agent-ws3-xxx (complete) - Cost: $0.79
+ Duration: 8m 23s
+
+🌊 Wave 2 (2 agents)
+ 🔄 agent-ws2-xxx (active) - Cost: $0.45
+ 🔄 agent-ws4-xxx (active) - Cost: $0.38
+ Elapsed: 3m 12s
+
+🌊 Wave 3 (1 agent)
+ ⏸️ agent-ws5-xxx (pending)
+
+🌊 Wave 4 (2 agents)
+ ⏸️ agent-ws6-xxx (pending)
+ ⏸️ agent-ws7-xxx (pending)
+
+💰 Total Cost: $3.48 / $50.00 (7%)
+⏱️ Total Time: 11m 35s
+
+Press Ctrl+C to pause monitoring (agents continue in background)
+```
+
+## Important Notes
+
+- **Non-blocking**: Agents run in background tmux sessions
+- **Resumable**: Can exit and resume with `/m-monitor `
+- **Auto-recovery**: Idle agents are killed automatically
+- **Budget limits**: Stops if budget exceeded
+- **Parallel execution**: Multiple agents per wave (up to max_concurrent)
+
+## Error Handling
+
+**If agent fails:**
+1. Mark agent as "failed"
+2. Continue other agents in wave
+3. Do not proceed to next wave
+4. Present failure summary to user
+5. Allow manual retry or skip
+
+**If timeout:**
+1. Check if agent is actually running (may be false positive)
+2. If truly stuck, kill and mark as failed
+3. Offer retry option
+
+## Resume Support
+
+**To resume a paused/stopped session:**
+
+```bash
+/m-implement --resume
+```
+
+**Resume logic:**
+1. Load existing session state
+2. Determine current wave
+3. Check which agents are still running
+4. Continue from where it left off
+
+## CLI Options (Future)
+
+```bash
+/m-implement [options]
+
+Options:
+ --resume Resume from last checkpoint
+ --from-wave N Start from specific wave number
+ --dry-run Show what would be executed
+ --max-concurrent N Override max concurrent agents
+ --no-monitoring Spawn agents and exit (no monitoring loop)
+```
+
+## Integration with `/spawn-agent`
+
+This command reuses logic from `~/.claude/commands/spawn-agent.md`:
+- Git worktree creation
+- Claude initialization detection
+- Task sending via tmux
+
+## Exit Conditions
+
+**Success:**
+- All waves complete
+- All agents have status "complete"
+- No failures
+
+**Failure:**
+- Any agent has status "failed"
+- Budget limit exceeded
+- User manually aborts
+
+**Pause:**
+- User presses Ctrl+C
+- Session state saved
+- Agents continue in background
+- Resume with `/m-monitor `
+
+---
+
+**End of `/m-implement` command**
diff --git a/claude-code-4.5/commands/m-monitor.md b/claude-code-4.5/commands/m-monitor.md
new file mode 100644
index 0000000..b1ecee6
--- /dev/null
+++ b/claude-code-4.5/commands/m-monitor.md
@@ -0,0 +1,118 @@
+---
+description: Multi-agent monitoring - Real-time dashboard for orchestration sessions
+tags: [orchestration, monitoring, multi-agent]
+---
+
+# Multi-Agent Monitoring (`/m-monitor`)
+
+You are now in **multi-agent monitoring mode**. Display a real-time dashboard of the orchestration session status.
+
+## Your Role
+
+Act as a **monitoring dashboard** that displays live status of all agents, waves, costs, and progress.
+
+## Usage
+
+```bash
+/m-monitor
+```
+
+## Display Format
+
+```
+🚀 Multi-Agent Session: orch-1763400000
+
+📊 Plan Summary:
+ - Task: Implement BigCommerce migration
+ - Created: 2025-11-17 10:00:00
+ - Total Workstreams: 7
+ - Total Waves: 4
+ - Max Concurrent: 4
+
+🌊 Wave 1: Complete ✅ (Duration: 8m 23s)
+ ✅ agent-ws1-1763338466 (WS-1: Service Layer)
+ Status: complete | Cost: $1.86 | Branch: feat/ws-1
+ Worktree: worktrees/ws-1-service-layer
+ Last Update: 2025-11-17 10:08:23
+
+ ✅ agent-ws3-1763338483 (WS-3: Database Schema)
+ Status: complete | Cost: $0.79 | Branch: feat/ws-3
+ Worktree: worktrees/ws-3-database-schema
+ Last Update: 2025-11-17 10:08:15
+
+🌊 Wave 2: Active 🔄 (Elapsed: 3m 12s)
+ 🔄 agent-ws2-1763341887 (WS-2: Edge Functions)
+ Status: active | Cost: $0.45 | Branch: feat/ws-2
+ Worktree: worktrees/ws-2-edge-functions
+ Last Update: 2025-11-17 10:11:35
+ Attach: tmux attach -t agent-ws2-1763341887
+
+ 🔄 agent-ws4-1763341892 (WS-4: Frontend UI)
+ Status: active | Cost: $0.38 | Branch: feat/ws-4
+ Worktree: worktrees/ws-4-frontend-ui
+ Last Update: 2025-11-17 10:11:42
+ Attach: tmux attach -t agent-ws4-1763341892
+
+🌊 Wave 3: Pending ⏸️
+ ⏸️ agent-ws5-pending (WS-5: Checkout Flow)
+
+🌊 Wave 4: Pending ⏸️
+ ⏸️ agent-ws6-pending (WS-6: E2E Tests)
+ ⏸️ agent-ws7-pending (WS-7: Documentation)
+
+💰 Budget Status:
+ - Current Cost: $3.48
+ - Budget Limit: $50.00
+ - Usage: 7% 🟢
+
+⏱️ Timeline:
+ - Total Elapsed: 11m 35s
+ - Estimated Remaining: ~5h 30m
+
+📋 Commands:
+ - Refresh: /m-monitor
+ - Attach to agent: tmux attach -t
+ - View agent output: tmux capture-pane -t -p
+ - Kill idle agent: ~/.claude/utils/orchestrator-agent.sh kill
+ - Pause session: Ctrl+C (agents continue in background)
+ - Resume session: /m-implement --resume
+
+Status Legend:
+ ✅ complete 🔄 active ⏸️ pending ⚠️ idle ❌ failed 💀 killed
+```
+
+## Implementation (Phase 2)
+
+**This is a stub command for Phase 1.** Full implementation in Phase 2 will include:
+
+1. **Live monitoring loop** - Refresh every 30s
+2. **Interactive controls** - Pause, resume, kill agents
+3. **Cost tracking** - Real-time budget updates
+4. **Idle detection** - Highlight idle agents
+5. **Failure alerts** - Notify on failures
+6. **Performance metrics** - Agent completion times
+
+## Current Workaround
+
+**Until Phase 2 is complete, use these manual commands:**
+
+```bash
+# View session status
+~/.claude/utils/orchestrator-state.sh print
+
+# List all agents
+~/.claude/utils/orchestrator-state.sh list-agents
+
+# Check specific agent
+~/.claude/utils/orchestrator-state.sh get-agent
+
+# Attach to agent tmux session
+tmux attach -t
+
+# View agent output without attaching
+tmux capture-pane -t -p | tail -50
+```
+
+---
+
+**End of `/m-monitor` command (stub)**
diff --git a/claude-code-4.5/commands/m-plan.md b/claude-code-4.5/commands/m-plan.md
new file mode 100644
index 0000000..39607c7
--- /dev/null
+++ b/claude-code-4.5/commands/m-plan.md
@@ -0,0 +1,261 @@
+---
+description: Multi-agent planning - Decompose complex tasks into parallel workstreams with dependency DAG
+tags: [orchestration, planning, multi-agent]
+---
+
+# Multi-Agent Planning (`/m-plan`)
+
+You are now in **multi-agent planning mode**. Your task is to decompose a complex task into parallel workstreams with a dependency graph (DAG).
+
+## Your Role
+
+Act as a **solution-architect** specialized in task decomposition and dependency analysis.
+
+## Process
+
+### 1. Understand the Task
+
+**Ask clarifying questions if needed:**
+- What is the overall goal?
+- Are there any constraints (time, budget, resources)?
+- Are there existing dependencies or requirements?
+- What is the desired merge strategy?
+
+### 2. Decompose into Workstreams
+
+**Break down the task into independent workstreams:**
+- Each workstream should be a cohesive unit of work
+- Workstreams should be as independent as possible
+- Identify clear deliverables for each workstream
+- Assign appropriate agent types (backend-developer, frontend-developer, etc.)
+
+**Workstream Guidelines:**
+- **Size**: Each workstream should take 1-3 hours of agent time
+- **Independence**: Minimize dependencies between workstreams
+- **Clarity**: Clear, specific deliverables
+- **Agent Type**: Match to specialized agent capabilities
+
+### 3. Identify Dependencies
+
+**For each workstream, determine:**
+- What other workstreams must complete first?
+- What outputs does it depend on?
+- What outputs does it produce for others?
+
+**Dependency Types:**
+- **Blocking**: Must complete before dependent can start
+- **Data**: Provides data/files needed by dependent
+- **Interface**: Provides API/interface contract
+
+### 4. Create DAG Structure
+
+**Generate a JSON DAG file:**
+```json
+{
+ "session_id": "orch-",
+ "created_at": "",
+ "task_description": "",
+ "nodes": {
+ "ws-1-": {
+ "task": "",
+ "agent_type": "backend-developer",
+ "workstream_id": "ws-1",
+ "dependencies": [],
+ "status": "pending",
+ "deliverables": [
+ "src/services/FooService.ts",
+ "tests for FooService"
+ ]
+ },
+ "ws-2-": {
+ "task": "",
+ "agent_type": "frontend-developer",
+ "workstream_id": "ws-2",
+ "dependencies": ["ws-1"],
+ "status": "pending",
+ "deliverables": [
+ "src/components/FooComponent.tsx"
+ ]
+ }
+ },
+ "edges": [
+ {"from": "ws-1", "to": "ws-2", "type": "blocking"}
+ ]
+}
+```
+
+### 5. Calculate Waves
+
+Use the topological sort utility to calculate execution waves:
+
+```bash
+~/.claude/utils/orchestrator-dag.sh topo-sort
+```
+
+**Add wave information to DAG:**
+```json
+{
+ "waves": [
+ {
+ "wave_number": 1,
+ "nodes": ["ws-1", "ws-3"],
+ "status": "pending",
+ "estimated_parallel_time_hours": 2
+ },
+ {
+ "wave_number": 2,
+ "nodes": ["ws-2", "ws-4"],
+ "status": "pending",
+ "estimated_parallel_time_hours": 1.5
+ }
+ ]
+}
+```
+
+### 6. Estimate Costs and Timeline
+
+**For each workstream:**
+- Estimate agent time (hours)
+- Estimate cost based on historical data (~$1-2 per hour)
+- Calculate total cost and timeline
+
+**Wave-based timeline:**
+- Wave 1: 2 hours (parallel)
+- Wave 2: 1.5 hours (parallel)
+- Total: 3.5 hours (not 7 hours due to parallelism)
+
+### 7. Save DAG File
+
+**Save to:**
+```
+~/.claude/orchestration/state/dag-.json
+```
+
+**Create orchestration session:**
+```bash
+SESSION_ID=$(~/.claude/utils/orchestrator-state.sh create \
+ "orch-$(date +%s)" \
+ "orch-$(date +%s)-monitor" \
+ '{}')
+
+echo "Created session: $SESSION_ID"
+```
+
+## Output Format
+
+**Present to user:**
+
+```markdown
+# Multi-Agent Plan:
+
+## Summary
+- **Total Workstreams**: X
+- **Total Waves**: Y
+- **Estimated Timeline**: Z hours (parallel)
+- **Estimated Cost**: $A - $B
+- **Max Concurrent Agents**: 4
+
+## Workstreams
+
+### Wave 1 (No dependencies)
+- **WS-1: ** (backend-developer) -
+ - Deliverables: ...
+ - Estimated: 2h, $2
+
+- **WS-3: ** (migration) -
+ - Deliverables: ...
+ - Estimated: 1.5h, $1.50
+
+### Wave 2 (Depends on Wave 1)
+- **WS-2: ** (backend-developer) -
+ - Dependencies: WS-3 (needs database schema)
+ - Deliverables: ...
+ - Estimated: 1.5h, $1.50
+
+### Wave 3 (Depends on Wave 2)
+- **WS-4: ** (frontend-developer) -
+ - Dependencies: WS-1 (needs service interface)
+ - Deliverables: ...
+ - Estimated: 2h, $2
+
+## Dependency Graph
+```
+ WS-1
+ │
+ ├─→ WS-2
+ │
+ WS-3
+ │
+ └─→ WS-4
+```
+
+## Timeline
+- Wave 1: 2h (WS-1, WS-3 in parallel)
+- Wave 2: 1.5h (WS-2 waits for WS-3)
+- Wave 3: 2h (WS-4 waits for WS-1)
+- **Total: 5.5 hours**
+
+## Total Cost Estimate
+- **Low**: $5.00 (efficient execution)
+- **High**: $8.00 (with retries)
+
+## DAG File
+Saved to: `~/.claude/orchestration/state/dag-.json`
+
+## Next Steps
+To execute this plan:
+```bash
+/m-implement
+```
+
+To monitor progress:
+```bash
+/m-monitor
+```
+```
+
+## Important Notes
+
+- **Keep workstreams focused**: Don't create too many tiny workstreams
+- **Minimize dependencies**: More parallelism = faster completion
+- **Assign correct agent types**: Use specialized agents for best results
+- **Include all deliverables**: Be specific about what each workstream produces
+- **Estimate conservatively**: Better to over-estimate than under-estimate
+
+## Agent Types Available
+
+- `backend-developer` - Server-side code, APIs, services
+- `frontend-developer` - UI components, React, TypeScript
+- `migration` - Database schemas, Flyway migrations
+- `test-writer-fixer` - E2E tests, test suites
+- `documentation-specialist` - Docs, runbooks, guides
+- `security-agent` - Security reviews, vulnerability fixes
+- `performance-optimizer` - Performance analysis, optimization
+- `devops-automator` - CI/CD, infrastructure, deployments
+
+## Example Usage
+
+**User Request:**
+```
+/m-plan Implement authentication system with OAuth, JWT tokens, and user profile management
+```
+
+**Your Response:**
+1. Ask clarifying questions (OAuth provider? Existing DB schema?)
+2. Decompose into workstreams (auth service, OAuth integration, user profiles, frontend UI)
+3. Identify dependencies (auth service → OAuth integration → frontend)
+4. Create DAG JSON
+5. Calculate waves
+6. Estimate costs
+7. Save DAG file
+8. Present plan to user
+9. Wait for approval before proceeding
+
+**After user approves:**
+- Do NOT execute automatically
+- Instruct user to run `/m-implement `
+- Provide monitoring commands
+
+---
+
+**End of `/m-plan` command**
diff --git a/claude-code-4.5/commands/merge-agent-work.md b/claude-code-4.5/commands/merge-agent-work.md
new file mode 100644
index 0000000..f54bc4f
--- /dev/null
+++ b/claude-code-4.5/commands/merge-agent-work.md
@@ -0,0 +1,30 @@
+# /merge-agent-work - Merge Agent Branch
+
+Merges an agent's branch into the current branch.
+
+## Usage
+
+```bash
+/merge-agent-work {timestamp}
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+AGENT_ID="$1"
+
+if [ -z "$AGENT_ID" ]; then
+ echo "❌ Agent ID required"
+ echo "Usage: /merge-agent-work {timestamp}"
+ exit 1
+fi
+
+# Source utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/../utils/git-worktree-utils.sh"
+
+# Merge agent work
+merge_agent_work "$AGENT_ID"
+```
diff --git a/claude-code-4.5/commands/spawn-agent.md b/claude-code-4.5/commands/spawn-agent.md
new file mode 100644
index 0000000..8ced240
--- /dev/null
+++ b/claude-code-4.5/commands/spawn-agent.md
@@ -0,0 +1,289 @@
+# /spawn-agent - Spawn Claude Agent in tmux Session
+
+Spawn a Claude Code agent in a separate tmux session with optional handover context.
+
+## Usage
+
+```bash
+/spawn-agent "implement user authentication"
+/spawn-agent "refactor the API layer" --with-handover
+/spawn-agent "implement feature X" --with-worktree
+/spawn-agent "review the PR" --with-worktree --with-handover
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+# Function: Wait for Claude Code to be ready for input
+wait_for_claude_ready() {
+ local SESSION=$1
+ local TIMEOUT=30
+ local START=$(date +%s)
+
+ echo "⏳ Waiting for Claude to initialize..."
+
+ while true; do
+ # Capture pane output (suppress errors if session not ready)
+ PANE_OUTPUT=$(tmux capture-pane -t "$SESSION" -p 2>/dev/null)
+
+ # Check for Claude prompt/splash (any of these indicates readiness)
+ if echo "$PANE_OUTPUT" | grep -qE "Claude Code|Welcome back|──────|Style:|bypass permissions"; then
+ # Verify not in error state
+ if ! echo "$PANE_OUTPUT" | grep -qiE "error|crash|failed|command not found"; then
+ echo "✅ Claude initialized successfully"
+ return 0
+ fi
+ fi
+
+ # Timeout check
+ local ELAPSED=$(($(date +%s) - START))
+ if [ $ELAPSED -gt $TIMEOUT ]; then
+ echo "❌ Timeout: Claude did not initialize within ${TIMEOUT}s"
+ echo "📋 Capturing debug output..."
+ tmux capture-pane -t "$SESSION" -p > "/tmp/spawn-agent-${SESSION}-failure.log" 2>&1
+ echo "Debug output saved to /tmp/spawn-agent-${SESSION}-failure.log"
+ return 1
+ fi
+
+ sleep 0.2
+ done
+}
+
+# Parse arguments
+TASK="$1"
+WITH_HANDOVER=false
+WITH_WORKTREE=false
+shift
+
+# Parse flags
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --with-handover)
+ WITH_HANDOVER=true
+ shift
+ ;;
+ --with-worktree)
+ WITH_WORKTREE=true
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [ -z "$TASK" ]; then
+ echo "❌ Task description required"
+ echo "Usage: /spawn-agent \"task description\" [--with-handover] [--with-worktree]"
+ exit 1
+fi
+
+# Generate session info
+TASK_ID=$(date +%s)
+SESSION="agent-${TASK_ID}"
+
+# Setup working directory (worktree or current)
+if [ "$WITH_WORKTREE" = true ]; then
+ # Detect transcrypt (informational only - works transparently with worktrees)
+ if git config --get-regexp '^transcrypt\.' >/dev/null 2>&1; then
+ echo "📦 Transcrypt detected - worktree will inherit encryption config automatically"
+ echo ""
+ fi
+
+ # Get current branch as base
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "HEAD")
+
+ # Generate task slug from task description
+ TASK_SLUG=$(echo "$TASK" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | tr -s ' ' '-' | cut -c1-40 | sed 's/-$//')
+
+ # Source worktree utilities
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ source "$SCRIPT_DIR/../utils/git-worktree-utils.sh"
+
+ # Create worktree with task slug
+ echo "🌳 Creating isolated git worktree..."
+ WORK_DIR=$(create_agent_worktree "$TASK_ID" "$CURRENT_BRANCH" "$TASK_SLUG")
+ AGENT_BRANCH="agent/agent-${TASK_ID}"
+
+ echo "✅ Worktree created:"
+ echo " Directory: $WORK_DIR"
+ echo " Branch: $AGENT_BRANCH"
+ echo " Base: $CURRENT_BRANCH"
+ echo ""
+else
+ WORK_DIR=$(pwd)
+ AGENT_BRANCH=""
+fi
+
+echo "🚀 Spawning Claude agent in tmux session..."
+echo ""
+
+# Generate handover if requested
+HANDOVER_CONTENT=""
+if [ "$WITH_HANDOVER" = true ]; then
+ echo "📝 Generating handover context..."
+
+ # Get current branch and recent commits
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
+ RECENT_COMMITS=$(git log --oneline -5 2>/dev/null || echo "No git history")
+ GIT_STATUS=$(git status -sb 2>/dev/null || echo "Not a git repo")
+
+ # Create handover content
+ HANDOVER_CONTENT=$(cat << EOF
+
+# Handover Context
+
+## Current State
+- Branch: $CURRENT_BRANCH
+- Directory: $WORK_DIR
+- Time: $(date)
+
+## Recent Commits
+$RECENT_COMMITS
+
+## Git Status
+$GIT_STATUS
+
+## Your Task
+$TASK
+
+---
+Please review the above context and proceed with the task.
+EOF
+)
+
+ echo "✅ Handover generated"
+ echo ""
+fi
+
+# Create tmux session
+tmux new-session -d -s "$SESSION" -c "$WORK_DIR"
+
+# Verify session creation
+if ! tmux has-session -t "$SESSION" 2>/dev/null; then
+ echo "❌ Failed to create tmux session"
+ exit 1
+fi
+
+echo "✅ Created tmux session: $SESSION"
+echo ""
+
+# Start Claude Code in the session
+tmux send-keys -t "$SESSION" "claude --dangerously-skip-permissions" C-m
+
+# Wait for Claude to be ready (not just sleep!)
+if ! wait_for_claude_ready "$SESSION"; then
+ echo "❌ Failed to start Claude agent - cleaning up..."
+ tmux kill-session -t "$SESSION" 2>/dev/null
+ exit 1
+fi
+
+# Additional small delay for UI stabilization
+sleep 0.5
+
+# Send handover context if generated (line-by-line to handle newlines)
+if [ "$WITH_HANDOVER" = true ]; then
+ echo "📤 Sending handover context to agent..."
+
+ # Send line-by-line to handle multi-line content properly
+ echo "$HANDOVER_CONTENT" | while IFS= read -r LINE || [ -n "$LINE" ]; do
+ # Use -l flag to send literal text (handles special characters)
+ tmux send-keys -t "$SESSION" -l "$LINE"
+ tmux send-keys -t "$SESSION" C-m
+ sleep 0.05 # Small delay between lines
+ done
+
+ # Final Enter to submit
+ tmux send-keys -t "$SESSION" C-m
+ sleep 0.5
+fi
+
+# Send the task (use literal mode for safety with special characters)
+echo "📤 Sending task to agent..."
+tmux send-keys -t "$SESSION" -l "$TASK"
+tmux send-keys -t "$SESSION" C-m
+
+# Small delay for Claude to start processing
+sleep 1
+
+# Verify task was received by checking if Claude is processing
+CURRENT_OUTPUT=$(tmux capture-pane -t "$SESSION" -p 2>/dev/null)
+if echo "$CURRENT_OUTPUT" | grep -qE "Thought for|Forming|Creating|Implement|⏳|✽|∴"; then
+ echo "✅ Task received and processing"
+elif echo "$CURRENT_OUTPUT" | grep -qE "error|failed|crash"; then
+ echo "⚠️ Warning: Detected error in agent output"
+ echo "📋 Last 10 lines of output:"
+ tmux capture-pane -t "$SESSION" -p | tail -10
+else
+ echo "ℹ️ Task sent (unable to confirm receipt - agent may still be starting)"
+fi
+
+echo ""
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "✨ Agent spawned successfully!"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo ""
+echo "Session: $SESSION"
+echo "Task: $TASK"
+echo "Directory: $WORK_DIR"
+echo ""
+echo "To monitor:"
+echo " tmux attach -t $SESSION"
+echo ""
+echo "To send more commands:"
+echo " tmux send-keys -t $SESSION \"your command\" C-m"
+echo ""
+echo "To kill session:"
+echo " tmux kill-session -t $SESSION"
+echo ""
+
+# Save metadata
+mkdir -p ~/.claude/agents
+cat > ~/.claude/agents/${SESSION}.json < /dev/null && echo "❌ adb not found. Is Android SDK installed?" && exit 1
+
+! emulator -list-avds 2>/dev/null | grep -q "^${DEVICE}$" && echo "❌ Emulator '$DEVICE' not found" && emulator -list-avds && exit 1
+
+RUNNING_EMULATOR=$(adb devices | grep "emulator" | cut -f1)
+
+if [ -z "$RUNNING_EMULATOR" ]; then
+ emulator -avd "$DEVICE" -no-snapshot-load -no-boot-anim &
+ adb wait-for-device
+ sleep 5
+ while [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do
+ sleep 2
+ done
+fi
+
+EMULATOR_SERIAL=$(adb devices | grep "emulator" | cut -f1 | head -1)
+```
+
+### Step 5: Setup Port Forwarding
+
+```bash
+# For dev server access from emulator
+if [ "$PROJECT_TYPE" = "react-native" ] || grep -q "\"dev\":" package.json 2>/dev/null; then
+ DEV_PORT=$(shuf -i 3000-9999 -n 1)
+ adb -s "$EMULATOR_SERIAL" reverse tcp:$DEV_PORT tcp:$DEV_PORT
+fi
+```
+
+### Step 6: Configure Poltergeist (Optional)
+
+```bash
+POLTERGEIST_AVAILABLE=false
+
+if command -v poltergeist &> /dev/null; then
+ POLTERGEIST_AVAILABLE=true
+
+ [ ! -f ".poltergeist.yml" ] && cat > .poltergeist.yml </dev/null; then
+ tmux new-window -t "$SESSION" -n dev-server
+ tmux send-keys -t "$SESSION:dev-server" "PORT=$DEV_PORT npm start | tee dev-server.log" C-m
+fi
+
+# Poltergeist (if available)
+if [ "$POLTERGEIST_AVAILABLE" = true ]; then
+ tmux new-window -t "$SESSION" -n poltergeist
+ tmux send-keys -t "$SESSION:poltergeist" "poltergeist watch --platform android | tee poltergeist.log" C-m
+fi
+
+# Logs
+tmux new-window -t "$SESSION" -n logs
+tmux send-keys -t "$SESSION:logs" "adb -s $EMULATOR_SERIAL logcat -v color" C-m
+
+# Git
+tmux new-window -t "$SESSION" -n git
+tmux send-keys -t "$SESSION:git" "git status" C-m
+```
+
+### Step 10: Save Metadata
+
+```bash
+cat > .tmux-android-session.json < /dev/null; then
+ POLTERGEIST_AVAILABLE=true
+
+ [ ! -f ".poltergeist.yml" ] && cat > .poltergeist.yml </dev/null; then
+ DEV_PORT=$(shuf -i 3000-9999 -n 1)
+ tmux new-window -t "$SESSION" -n dev-server
+ tmux send-keys -t "$SESSION:dev-server" "PORT=$DEV_PORT npm start | tee dev-server.log" C-m
+fi
+
+# Poltergeist (if available)
+if [ "$POLTERGEIST_AVAILABLE" = true ]; then
+ tmux new-window -t "$SESSION" -n poltergeist
+ tmux send-keys -t "$SESSION:poltergeist" "poltergeist watch --platform ios | tee poltergeist.log" C-m
+fi
+
+# Logs
+tmux new-window -t "$SESSION" -n logs
+tmux send-keys -t "$SESSION:logs" "xcrun simctl spawn $SIMULATOR_UDID log stream --level debug" C-m
+
+# Git
+tmux new-window -t "$SESSION" -n git
+tmux send-keys -t "$SESSION:git" "git status" C-m
+```
+
+### Step 9: Save Metadata
+
+```bash
+cat > .tmux-ios-session.json </dev/null
+ exit 1
+fi
+```
+
+### Step 2: Detect Project Type
+
+```bash
+detect_project_type() {
+ if [ -f "package.json" ]; then
+ grep -q "\"next\":" package.json && echo "nextjs" && return
+ grep -q "\"vite\":" package.json && echo "vite" && return
+ grep -q "\"react-scripts\":" package.json && echo "cra" && return
+ grep -q "\"@vue/cli\":" package.json && echo "vue" && return
+ echo "node"
+ elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
+ grep -q "django" requirements.txt pyproject.toml 2>/dev/null && echo "django" && return
+ grep -q "flask" requirements.txt pyproject.toml 2>/dev/null && echo "flask" && return
+ echo "python"
+ elif [ -f "Cargo.toml" ]; then
+ echo "rust"
+ elif [ -f "go.mod" ]; then
+ echo "go"
+ else
+ echo "unknown"
+ fi
+}
+
+PROJECT_TYPE=$(detect_project_type)
+```
+
+### Step 3: Detect Required Services
+
+```bash
+NEEDS_SUPABASE=false
+NEEDS_POSTGRES=false
+NEEDS_REDIS=false
+
+[ -f "supabase/config.toml" ] && NEEDS_SUPABASE=true
+grep -q "postgres" "$ENV_FILE" 2>/dev/null && NEEDS_POSTGRES=true
+grep -q "redis" "$ENV_FILE" 2>/dev/null && NEEDS_REDIS=true
+```
+
+### Step 4: Generate Random Port
+
+```bash
+DEV_PORT=$(shuf -i 3000-9999 -n 1)
+
+while lsof -i :$DEV_PORT >/dev/null 2>&1; do
+ DEV_PORT=$(shuf -i 3000-9999 -n 1)
+done
+```
+
+### Step 5: Create tmux Session
+
+```bash
+PROJECT_NAME=$(basename "$(pwd)")
+TIMESTAMP=$(date +%s)
+SESSION="dev-${PROJECT_NAME}-${TIMESTAMP}"
+
+tmux new-session -d -s "$SESSION" -n servers
+```
+
+### Step 6: Start Services
+
+```bash
+PANE_COUNT=0
+
+# Main dev server
+case $PROJECT_TYPE in
+ nextjs|vite|cra|vue)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "PORT=$DEV_PORT npm run dev | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+ django)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "python manage.py runserver $DEV_PORT | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+ flask)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "FLASK_RUN_PORT=$DEV_PORT flask run | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+ *)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "PORT=$DEV_PORT npm run dev | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+esac
+
+# Additional services (if needed)
+if [ "$NEEDS_SUPABASE" = true ]; then
+ PANE_COUNT=$((PANE_COUNT + 1))
+ tmux split-window -v -t "$SESSION:servers"
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "supabase start" C-m
+fi
+
+if [ "$NEEDS_POSTGRES" = true ] && [ "$NEEDS_SUPABASE" = false ]; then
+ PANE_COUNT=$((PANE_COUNT + 1))
+ tmux split-window -v -t "$SESSION:servers"
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "docker-compose up postgres" C-m
+fi
+
+if [ "$NEEDS_REDIS" = true ]; then
+ PANE_COUNT=$((PANE_COUNT + 1))
+ tmux split-window -v -t "$SESSION:servers"
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "redis-server" C-m
+fi
+
+tmux select-layout -t "$SESSION:servers" tiled
+```
+
+### Step 7: Create Additional Windows
+
+```bash
+# Logs window
+tmux new-window -t "$SESSION" -n logs
+tmux send-keys -t "$SESSION:logs" "tail -f dev-server-${DEV_PORT}.log 2>/dev/null || sleep infinity" C-m
+
+# Work window
+tmux new-window -t "$SESSION" -n work
+
+# Git window
+tmux new-window -t "$SESSION" -n git
+tmux send-keys -t "$SESSION:git" "git status" C-m
+```
+
+### Step 8: Save Metadata
+
+```bash
+cat > .tmux-dev-session.json <
+```
+
+**Long-running detached sessions**:
+```
+💡 Found dev sessions running >2 hours
+Recommendation: Check if still needed: tmux attach -t
+```
+
+**Many sessions (>5)**:
+```
+🧹 Found 5+ active sessions
+Recommendation: Review and clean up unused sessions
+```
+
+## Use Cases
+
+### Before Starting New Environment
+
+```bash
+/tmux-status
+# Check for port conflicts and existing sessions before /start-local
+```
+
+### Monitor Agent Progress
+
+```bash
+/tmux-status
+# See status of spawned agents (running, completed, etc.)
+```
+
+### Session Discovery
+
+```bash
+/tmux-status --detailed
+# Find specific session by project name or port
+```
+
+## Notes
+
+- Read-only, never modifies sessions
+- Uses tmux-monitor skill for discovery
+- Integrates with tmuxwatch if available
+- Detects metadata from `.tmux-dev-session.json` and `~/.claude/agents/*.json`
diff --git a/claude-code-4.5/orchestration/state/config.json b/claude-code-4.5/orchestration/state/config.json
new file mode 100644
index 0000000..32484e6
--- /dev/null
+++ b/claude-code-4.5/orchestration/state/config.json
@@ -0,0 +1,24 @@
+{
+ "orchestrator": {
+ "max_concurrent_agents": 4,
+ "idle_timeout_minutes": 15,
+ "checkpoint_interval_minutes": 5,
+ "max_retry_attempts": 3,
+ "polling_interval_seconds": 30
+ },
+ "merge": {
+ "default_strategy": "sequential",
+ "require_tests": true,
+ "auto_merge": false
+ },
+ "monitoring": {
+ "check_interval_seconds": 30,
+ "log_level": "info",
+ "enable_cost_tracking": true
+ },
+ "resource_limits": {
+ "max_budget_usd": 50,
+ "warn_at_percent": 80,
+ "hard_stop_at_percent": 100
+ }
+}
diff --git a/claude-code-4.5/skills/frontend-design/SKILL.md b/claude-code-4.5/skills/frontend-design/SKILL.md
new file mode 100644
index 0000000..a928b72
--- /dev/null
+++ b/claude-code-4.5/skills/frontend-design/SKILL.md
@@ -0,0 +1,145 @@
+---
+name: frontend-design
+description: Frontend design skill for UI/UX implementation - generates distinctive, production-grade interfaces
+version: 1.0.0
+authors:
+ - Prithvi Rajasekaran
+ - Alexander Bricken
+---
+
+# Frontend Design Skill
+
+This skill helps create **distinctive, production-grade frontend interfaces** that avoid generic AI aesthetics.
+
+## Core Principles
+
+When building any frontend interface, follow these principles to create visually striking, memorable designs:
+
+### 1. Establish Bold Aesthetic Direction
+
+**Before writing any code**, define a clear aesthetic vision:
+
+- **Understand the purpose**: What is this interface trying to achieve?
+- **Choose an extreme tone**: Select a distinctive aesthetic direction
+ - Brutalist: Raw, bold, functional
+ - Maximalist: Rich, layered, decorative
+ - Retro-futuristic: Nostalgic tech aesthetics
+ - Minimalist with impact: Powerful simplicity
+ - Neo-brutalist: Modern take on brutalism
+- **Identify the unforgettable element**: What will make this design memorable?
+
+### 2. Implementation Standards
+
+Every interface you create should be:
+
+- ✅ **Production-grade and functional**: Code that works flawlessly
+- ✅ **Visually striking and memorable**: Designs that stand out
+- ✅ **Cohesive with clear aesthetic point-of-view**: Unified vision throughout
+
+## Critical Design Guidelines
+
+### Typography
+
+**Choose fonts that are beautiful, unique, and interesting.**
+
+- ❌ **AVOID**: Generic system fonts (Arial, Helvetica, default sans-serif)
+- ✅ **USE**: Distinctive choices that elevate aesthetics
+ - Display fonts with character
+ - Unexpected font pairings
+ - Variable fonts for dynamic expression
+ - Fonts that reinforce your aesthetic direction
+
+### Color & Theme
+
+**Commit to cohesive aesthetics with CSS variables.**
+
+- ❌ **AVOID**: Generic color palettes, predictable combinations
+- ✅ **USE**: Dominant colors with sharp accents
+ - Define comprehensive CSS custom properties
+ - Create mood through color temperature
+ - Use unexpected color combinations
+ - Build depth with tints, shades, and tones
+
+### Motion & Animation
+
+**Use high-impact animations that enhance the experience.**
+
+- For **HTML/CSS**: CSS-only animations (transforms, transitions, keyframes)
+- For **React**: Motion library (Framer Motion, React Spring)
+- ❌ **AVOID**: Generic fade-ins, boring transitions
+- ✅ **USE**: High-impact moments
+ - Purposeful movement that guides attention
+ - Smooth, performant animations
+ - Delightful micro-interactions
+ - Entrance/exit animations with personality
+
+### Composition & Layout
+
+**Embrace unexpected layouts.**
+
+- ❌ **AVOID**: Predictable grids, centered everything, safe layouts
+- ✅ **USE**: Bold composition choices
+ - Asymmetry
+ - Overlap
+ - Diagonal flow
+ - Unexpected whitespace
+ - Breaking the grid intentionally
+
+### Details & Atmosphere
+
+**Create atmosphere through thoughtful details.**
+
+- ✅ Textures and grain
+- ✅ Sophisticated gradients
+- ✅ Patterns and backgrounds
+- ✅ Custom effects (blur, glow, shadows)
+- ✅ Attention to spacing and rhythm
+
+## What to AVOID
+
+**Generic AI Design Patterns:**
+
+- ❌ Overused fonts (Inter, Roboto, Open Sans as defaults)
+- ❌ Clichéd color schemes (purple gradients, generic blues)
+- ❌ Predictable layouts (everything centered, safe grids)
+- ❌ Cookie-cutter design that lacks context-specific character
+- ❌ Lack of personality or point-of-view
+- ❌ Generic animations (basic fade-ins everywhere)
+
+## Execution Philosophy
+
+**Show restraint or elaboration as the vision demands—execution quality matters most.**
+
+- Every design decision should serve the aesthetic direction
+- Don't add complexity for its own sake
+- Don't oversimplify when richness is needed
+- Commit fully to your chosen direction
+- Polish details relentlessly
+
+## Implementation Process
+
+When creating a frontend interface:
+
+1. **Define the aesthetic direction** (brutalist, maximalist, minimalist, etc.)
+2. **Choose distinctive typography** that reinforces the aesthetic
+3. **Establish color system** with CSS variables
+4. **Design layout** with unexpected but purposeful composition
+5. **Add motion** that enhances key moments
+6. **Polish details** (textures, shadows, spacing)
+7. **Review against principles** - is this distinctive and production-grade?
+
+## Examples of Strong Aesthetic Directions
+
+- **Brutalist Dashboard**: Monospace fonts, high contrast, grid-based, utilitarian
+- **Retro-Futuristic Landing**: Neon colors, chrome effects, 80s sci-fi inspired
+- **Minimalist with Impact**: Generous whitespace, bold typography, single accent color
+- **Neo-Brutalist App**: Raw aesthetics, asymmetric layouts, bold shadows
+- **Maximalist Content**: Rich layers, decorative elements, abundant color
+
+## Resources
+
+For deeper guidance on prompting for high-quality frontend design, see the [Frontend Aesthetics Cookbook](https://github.com/anthropics/claude-cookbooks/blob/main/coding/prompting_for_frontend_aesthetics.ipynb).
+
+---
+
+**Remember**: The goal is to create interfaces that are both functionally excellent and visually unforgettable. Avoid generic AI aesthetics by committing to a clear, bold direction and executing it with meticulous attention to detail.
diff --git a/claude-code-4.5/skills/tmux-monitor/SKILL.md b/claude-code-4.5/skills/tmux-monitor/SKILL.md
new file mode 100644
index 0000000..42cb23c
--- /dev/null
+++ b/claude-code-4.5/skills/tmux-monitor/SKILL.md
@@ -0,0 +1,370 @@
+---
+name: tmux-monitor
+description: Monitor and report status of all tmux sessions including dev environments, spawned agents, and running processes. Uses tmuxwatch for enhanced visibility.
+version: 1.0.0
+---
+
+# tmux-monitor Skill
+
+## Purpose
+
+Provide comprehensive visibility into all active tmux sessions, running processes, and spawned agents. This skill enables checking what's running where without needing to manually inspect each session.
+
+## Capabilities
+
+1. **Session Discovery**: Find and categorize all tmux sessions
+2. **Process Inspection**: Identify running servers, dev environments, agents
+3. **Port Mapping**: Show which ports are in use and by what
+4. **Status Reporting**: Generate detailed reports with recommendations
+5. **tmuxwatch Integration**: Use tmuxwatch for enhanced real-time monitoring
+6. **Metadata Extraction**: Read session metadata from .tmux-dev-session.json and agent JSON files
+
+## When to Use
+
+- User asks "what's running?"
+- Before starting new dev environments (check port conflicts)
+- After spawning agents (verify they started correctly)
+- When debugging server/process issues
+- Before session cleanup
+- When context switching between projects
+
+## Implementation
+
+### Step 1: Check tmux Availability
+
+```bash
+if ! command -v tmux &> /dev/null; then
+ echo "❌ tmux is not installed"
+ exit 1
+fi
+
+if ! tmux list-sessions 2>/dev/null; then
+ echo "✅ No tmux sessions currently running"
+ exit 0
+fi
+```
+
+### Step 2: Discover All Sessions
+
+```bash
+# Get all sessions with metadata
+SESSIONS=$(tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}')
+
+# Count sessions
+TOTAL_SESSIONS=$(echo "$SESSIONS" | wc -l | tr -d ' ')
+```
+
+### Step 3: Categorize Sessions
+
+Group by prefix pattern:
+
+- `dev-*` → Development environments
+- `agent-*` → Spawned agents
+- `claude-*` → Claude Code sessions
+- `monitor-*` → Monitoring sessions
+- Others → Miscellaneous
+
+```bash
+DEV_SESSIONS=$(echo "$SESSIONS" | grep "^dev-" || true)
+AGENT_SESSIONS=$(echo "$SESSIONS" | grep "^agent-" || true)
+CLAUDE_SESSIONS=$(echo "$SESSIONS" | grep "^claude-" || true)
+```
+
+### Step 4: Extract Details for Each Session
+
+For each session, gather:
+
+**Window Information**:
+```bash
+tmux list-windows -t "$SESSION" -F '#{window_index}:#{window_name}:#{window_panes}'
+```
+
+**Running Processes** (from first pane of each window):
+```bash
+tmux capture-pane -t "$SESSION:0.0" -p -S -10 -E 0
+```
+
+**Port Detection** (check for listening ports):
+```bash
+# Extract ports from session metadata
+if [ -f ".tmux-dev-session.json" ]; then
+ BACKEND_PORT=$(jq -r '.backend.port // empty' .tmux-dev-session.json)
+ FRONTEND_PORT=$(jq -r '.frontend.port // empty' .tmux-dev-session.json)
+fi
+
+# Or detect from process list
+lsof -nP -iTCP -sTCP:LISTEN | grep -E "node|python|uv|npm"
+```
+
+### Step 5: Load Session Metadata
+
+**Dev Environment Metadata** (`.tmux-dev-session.json`):
+```bash
+if [ -f ".tmux-dev-session.json" ]; then
+ PROJECT=$(jq -r '.project' .tmux-dev-session.json)
+ TYPE=$(jq -r '.type' .tmux-dev-session.json)
+ BACKEND_PORT=$(jq -r '.backend.port // "N/A"' .tmux-dev-session.json)
+ FRONTEND_PORT=$(jq -r '.frontend.port // "N/A"' .tmux-dev-session.json)
+ CREATED=$(jq -r '.created' .tmux-dev-session.json)
+fi
+```
+
+**Agent Metadata** (`~/.claude/agents/*.json`):
+```bash
+if [ -f "$HOME/.claude/agents/${SESSION}.json" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type' "$HOME/.claude/agents/${SESSION}.json")
+ TASK=$(jq -r '.task' "$HOME/.claude/agents/${SESSION}.json")
+ STATUS=$(jq -r '.status' "$HOME/.claude/agents/${SESSION}.json")
+ DIRECTORY=$(jq -r '.directory' "$HOME/.claude/agents/${SESSION}.json")
+ CREATED=$(jq -r '.created' "$HOME/.claude/agents/${SESSION}.json")
+fi
+```
+
+### Step 6: tmuxwatch Integration
+
+If tmuxwatch is available, offer enhanced view:
+
+```bash
+if command -v tmuxwatch &> /dev/null; then
+ echo ""
+ echo "📊 Enhanced Monitoring Available:"
+ echo " Real-time TUI: tmuxwatch"
+ echo " JSON export: tmuxwatch --dump | jq"
+ echo ""
+
+ # Optional: Use tmuxwatch for structured data
+ TMUXWATCH_DATA=$(tmuxwatch --dump 2>/dev/null || echo "{}")
+fi
+```
+
+### Step 7: Generate Comprehensive Report
+
+```markdown
+# tmux Sessions Overview
+
+**Total Active Sessions**: {count}
+**Total Windows**: {window_count}
+**Total Panes**: {pane_count}
+
+---
+
+## Development Environments ({dev_count})
+
+### 1. dev-myapp-1705161234
+- **Type**: fullstack
+- **Project**: myapp
+- **Status**: ⚡ Active (attached)
+- **Windows**: 4 (servers, logs, claude-work, git)
+- **Panes**: 8
+- **Backend**: Port 8432 → http://localhost:8432
+- **Frontend**: Port 3891 → http://localhost:3891
+- **Created**: 2025-01-13 14:30:00 (2h ago)
+- **Attach**: `tmux attach -t dev-myapp-1705161234`
+
+---
+
+## Spawned Agents ({agent_count})
+
+### 2. agent-1705160000
+- **Agent Type**: codex
+- **Task**: Refactor authentication module
+- **Status**: ⚙️ Running (15 minutes)
+- **Working Directory**: /Users/stevie/projects/myapp
+- **Git Worktree**: worktrees/agent-1705160000
+- **Windows**: 1 (work)
+- **Panes**: 2 (agent | monitoring)
+- **Last Output**: "Analyzing auth.py dependencies..."
+- **Attach**: `tmux attach -t agent-1705160000`
+- **Metadata**: `~/.claude/agents/agent-1705160000.json`
+
+### 3. agent-1705161000
+- **Agent Type**: aider
+- **Task**: Generate API documentation
+- **Status**: ✅ Completed (5 minutes ago)
+- **Output**: Documentation written to docs/api/
+- **Attach**: `tmux attach -t agent-1705161000` (review)
+- **Cleanup**: `tmux kill-session -t agent-1705161000`
+
+---
+
+## Running Processes Summary
+
+| Port | Service | Session | Status |
+|------|--------------|--------------------------|---------|
+| 8432 | Backend API | dev-myapp-1705161234 | Running |
+| 3891 | Frontend Dev | dev-myapp-1705161234 | Running |
+| 5160 | Supabase | dev-shotclubhouse-xxx | Running |
+
+---
+
+## Quick Actions
+
+**Attach to session**:
+```bash
+tmux attach -t
+```
+
+**Kill session**:
+```bash
+tmux kill-session -t
+```
+
+**List all sessions**:
+```bash
+tmux ls
+```
+
+**Kill all completed agents**:
+```bash
+for session in $(tmux ls | grep "^agent-" | cut -d: -f1); do
+ STATUS=$(jq -r '.status' "$HOME/.claude/agents/${session}.json" 2>/dev/null)
+ if [ "$STATUS" = "completed" ]; then
+ tmux kill-session -t "$session"
+ fi
+done
+```
+
+---
+
+## Recommendations
+
+{generated based on findings}
+```
+
+### Step 8: Provide Contextual Recommendations
+
+**If completed agents found**:
+```
+⚠️ Found 1 completed agent session:
+ - agent-1705161000: Task completed 5 minutes ago
+
+Recommendation: Review results and clean up:
+ tmux attach -t agent-1705161000 # Review
+ tmux kill-session -t agent-1705161000 # Cleanup
+```
+
+**If long-running detached sessions**:
+```
+💡 Found detached session running for 2h 40m:
+ - dev-api-service-1705159000
+
+Recommendation: Check if still needed:
+ tmux attach -t dev-api-service-1705159000
+```
+
+**If port conflicts detected**:
+```
+⚠️ Port conflict detected:
+ - Port 3000 in use by dev-oldproject-xxx
+ - New session will use random port instead
+
+Recommendation: Clean up old session if no longer needed
+```
+
+## Output Formats
+
+### Compact (Default)
+
+```
+5 active sessions:
+- dev-myapp-1705161234 (fullstack, 4 windows, active)
+- dev-api-service-1705159000 (backend-only, 4 windows, detached)
+- agent-1705160000 (codex, running 15m)
+- agent-1705161000 (aider, completed ✓)
+- claude-work (main session, current)
+
+3 running servers:
+- Port 8432: Backend API (dev-myapp)
+- Port 3891: Frontend Dev (dev-myapp)
+- Port 5160: Supabase (dev-shotclubhouse)
+```
+
+### Detailed (Verbose)
+
+Full report with all metadata, sample output, recommendations.
+
+### JSON (Programmatic)
+
+```json
+{
+ "sessions": [
+ {
+ "name": "dev-myapp-1705161234",
+ "type": "dev-environment",
+ "category": "fullstack",
+ "windows": 4,
+ "panes": 8,
+ "status": "attached",
+ "created": "2025-01-13T14:30:00Z",
+ "ports": {
+ "backend": 8432,
+ "frontend": 3891
+ },
+ "metadata_file": ".tmux-dev-session.json"
+ },
+ {
+ "name": "agent-1705160000",
+ "type": "spawned-agent",
+ "agent_type": "codex",
+ "task": "Refactor authentication module",
+ "status": "running",
+ "runtime": "15m",
+ "directory": "/Users/stevie/projects/myapp",
+ "worktree": "worktrees/agent-1705160000",
+ "metadata_file": "~/.claude/agents/agent-1705160000.json"
+ }
+ ],
+ "summary": {
+ "total_sessions": 5,
+ "total_windows": 12,
+ "total_panes": 28,
+ "running_servers": 3,
+ "active_agents": 1,
+ "completed_agents": 1
+ },
+ "ports": [
+ {"port": 8432, "service": "Backend API", "session": "dev-myapp-1705161234"},
+ {"port": 3891, "service": "Frontend Dev", "session": "dev-myapp-1705161234"},
+ {"port": 5160, "service": "Supabase", "session": "dev-shotclubhouse-xxx"}
+ ]
+}
+```
+
+## Integration with Commands
+
+This skill is used by:
+- `/tmux-status` command (user-facing command)
+- Automatically before starting new dev environments (conflict detection)
+- By spawned agents to check session status
+
+## Dependencies
+
+- `tmux` (required)
+- `jq` (required for JSON parsing)
+- `lsof` (optional, for port detection)
+- `tmuxwatch` (optional, for enhanced monitoring)
+
+## File Structure
+
+```
+~/.claude/agents/
+ agent-{timestamp}.json # Agent metadata
+
+.tmux-dev-session.json # Dev environment metadata (per project)
+
+/tmp/tmux-monitor-cache.json # Optional cache for performance
+```
+
+## Related Commands
+
+- `/tmux-status` - User-facing wrapper around this skill
+- `/spawn-agent` - Creates sessions that this skill monitors
+- `/start-local`, `/start-ios`, `/start-android` - Create dev environments
+
+## Notes
+
+- This skill is read-only, never modifies sessions
+- Safe to run anytime without side effects
+- Provides snapshot of current state
+- Can be cached for performance (TTL: 10 seconds)
+- Should be run before potentially conflicting operations
diff --git a/claude-code-4.5/skills/tmux-monitor/scripts/monitor.sh b/claude-code-4.5/skills/tmux-monitor/scripts/monitor.sh
new file mode 100755
index 0000000..0b1d3f5
--- /dev/null
+++ b/claude-code-4.5/skills/tmux-monitor/scripts/monitor.sh
@@ -0,0 +1,417 @@
+#!/bin/bash
+
+# ABOUTME: tmux session monitoring script - discovers, categorizes, and reports status of all active tmux sessions
+
+set -euo pipefail
+
+# Output mode: compact (default), detailed, json
+OUTPUT_MODE="${1:-compact}"
+
+# Check if tmux is available
+if ! command -v tmux &> /dev/null; then
+ echo "❌ tmux is not installed"
+ exit 1
+fi
+
+# Check if there are any sessions
+if ! tmux list-sessions 2>/dev/null | grep -q .; then
+ if [ "$OUTPUT_MODE" = "json" ]; then
+ echo '{"sessions": [], "summary": {"total_sessions": 0, "total_windows": 0, "total_panes": 0}}'
+ else
+ echo "✅ No tmux sessions currently running"
+ fi
+ exit 0
+fi
+
+# Initialize counters
+TOTAL_SESSIONS=0
+TOTAL_WINDOWS=0
+TOTAL_PANES=0
+
+# Arrays to store sessions by category
+declare -a DEV_SESSIONS
+declare -a AGENT_SESSIONS
+declare -a MONITOR_SESSIONS
+declare -a CLAUDE_SESSIONS
+declare -a OTHER_SESSIONS
+
+# Get all sessions
+SESSIONS=$(tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}' 2>/dev/null)
+
+# Parse and categorize sessions
+while IFS='|' read -r SESSION_NAME WINDOW_COUNT CREATED ATTACHED; do
+ TOTAL_SESSIONS=$((TOTAL_SESSIONS + 1))
+ TOTAL_WINDOWS=$((TOTAL_WINDOWS + WINDOW_COUNT))
+
+ # Get pane count for this session
+ PANE_COUNT=$(tmux list-panes -t "$SESSION_NAME" 2>/dev/null | wc -l | tr -d ' ')
+ TOTAL_PANES=$((TOTAL_PANES + PANE_COUNT))
+
+ # Categorize by prefix
+ if [[ "$SESSION_NAME" == dev-* ]]; then
+ DEV_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ elif [[ "$SESSION_NAME" == agent-* ]]; then
+ AGENT_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ elif [[ "$SESSION_NAME" == monitor-* ]]; then
+ MONITOR_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ elif [[ "$SESSION_NAME" == claude-* ]] || [[ "$SESSION_NAME" == *claude* ]]; then
+ CLAUDE_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ else
+ OTHER_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ fi
+done <<< "$SESSIONS"
+
+# Helper function to get session metadata
+get_dev_metadata() {
+ local SESSION_NAME=$1
+ local METADATA_FILE=".tmux-dev-session.json"
+
+ if [ -f "$METADATA_FILE" ]; then
+ local SESSION_IN_FILE=$(jq -r '.session // empty' "$METADATA_FILE" 2>/dev/null)
+ if [ "$SESSION_IN_FILE" = "$SESSION_NAME" ]; then
+ echo "$METADATA_FILE"
+ fi
+ fi
+
+ # Try iOS-specific metadata
+ if [ -f ".tmux-ios-session.json" ]; then
+ local SESSION_IN_FILE=$(jq -r '.session // empty' ".tmux-ios-session.json" 2>/dev/null)
+ if [ "$SESSION_IN_FILE" = "$SESSION_NAME" ]; then
+ echo ".tmux-ios-session.json"
+ fi
+ fi
+
+ # Try Android-specific metadata
+ if [ -f ".tmux-android-session.json" ]; then
+ local SESSION_IN_FILE=$(jq -r '.session // empty' ".tmux-android-session.json" 2>/dev/null)
+ if [ "$SESSION_IN_FILE" = "$SESSION_NAME" ]; then
+ echo ".tmux-android-session.json"
+ fi
+ fi
+}
+
+get_agent_metadata() {
+ local SESSION_NAME=$1
+ local METADATA_FILE="$HOME/.claude/agents/${SESSION_NAME}.json"
+
+ if [ -f "$METADATA_FILE" ]; then
+ echo "$METADATA_FILE"
+ fi
+}
+
+# Get running ports
+get_running_ports() {
+ if command -v lsof &> /dev/null; then
+ lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -E "node|python|uv|npm|ruby|java" | awk '{print $9}' | cut -d':' -f2 | sort -u || true
+ fi
+}
+
+RUNNING_PORTS=$(get_running_ports)
+
+# Output functions
+
+output_compact() {
+ echo "${TOTAL_SESSIONS} active sessions:"
+
+ # Dev environments
+ if [[ -v DEV_SESSIONS[@] ]] && [ ${#DEV_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${DEV_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="detached"
+ [ "$ATTACHED" = "1" ] && STATUS="active"
+
+ # Try to get metadata
+ METADATA_FILE=$(get_dev_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ PROJECT_TYPE=$(jq -r '.type // "dev"' "$METADATA_FILE" 2>/dev/null)
+ echo "- $SESSION_NAME ($PROJECT_TYPE, $WINDOW_COUNT windows, $STATUS)"
+ else
+ echo "- $SESSION_NAME ($WINDOW_COUNT windows, $STATUS)"
+ fi
+ done
+ fi
+
+ # Agent sessions
+ if [[ -v AGENT_SESSIONS[@] ]] && [ ${#AGENT_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${AGENT_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ # Try to get agent metadata
+ METADATA_FILE=$(get_agent_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ STATUS_=$(jq -r '.status // "running"' "$METADATA_FILE" 2>/dev/null)
+ echo "- $SESSION_NAME ($AGENT_TYPE, $STATUS_)"
+ else
+ echo "- $SESSION_NAME (agent)"
+ fi
+ done
+ fi
+
+ # Claude sessions
+ if [[ -v CLAUDE_SESSIONS[@] ]] && [ ${#CLAUDE_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${CLAUDE_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="detached"
+ [ "$ATTACHED" = "1" ] && STATUS="current"
+ echo "- $SESSION_NAME (main session, $STATUS)"
+ done
+ fi
+
+ # Other sessions
+ if [[ -v OTHER_SESSIONS[@] ]] && [ ${#OTHER_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${OTHER_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ echo "- $SESSION_NAME ($WINDOW_COUNT windows)"
+ done
+ fi
+
+ # Port summary
+ if [ -n "$RUNNING_PORTS" ]; then
+ PORT_COUNT=$(echo "$RUNNING_PORTS" | wc -l | tr -d ' ')
+ echo ""
+ echo "$PORT_COUNT running servers on ports: $(echo $RUNNING_PORTS | tr '\n' ',' | sed 's/,$//')"
+ fi
+
+ echo ""
+ echo "Use /tmux-status --detailed for full report"
+}
+
+output_detailed() {
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "📊 tmux Sessions Overview"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "**Total Active Sessions**: $TOTAL_SESSIONS"
+ echo "**Total Windows**: $TOTAL_WINDOWS"
+ echo "**Total Panes**: $TOTAL_PANES"
+ echo ""
+
+ # Dev environments
+ if [[ -v DEV_SESSIONS[@] ]] && [ ${#DEV_SESSIONS[@]} -gt 0 ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Development Environments (${#DEV_SESSIONS[@]})"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ local INDEX=1
+ for session_data in "${DEV_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="🔌 Detached"
+ [ "$ATTACHED" = "1" ] && STATUS="⚡ Active (attached)"
+
+ echo "### $INDEX. $SESSION_NAME"
+ echo "- **Status**: $STATUS"
+ echo "- **Windows**: $WINDOW_COUNT"
+ echo "- **Panes**: $PANE_COUNT"
+
+ # Get metadata if available
+ METADATA_FILE=$(get_dev_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ PROJECT=$(jq -r '.project // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ PROJECT_TYPE=$(jq -r '.type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ CREATED=$(jq -r '.created // "unknown"' "$METADATA_FILE" 2>/dev/null)
+
+ echo "- **Project**: $PROJECT ($PROJECT_TYPE)"
+ echo "- **Created**: $CREATED"
+
+ # Check for ports
+ if jq -e '.dev_port' "$METADATA_FILE" &>/dev/null; then
+ DEV_PORT=$(jq -r '.dev_port' "$METADATA_FILE" 2>/dev/null)
+ echo "- **Dev Server**: http://localhost:$DEV_PORT"
+ fi
+
+ if jq -e '.services' "$METADATA_FILE" &>/dev/null; then
+ echo "- **Services**: $(jq -r '.services | keys | join(", ")' "$METADATA_FILE" 2>/dev/null)"
+ fi
+ fi
+
+ echo "- **Attach**: \`tmux attach -t $SESSION_NAME\`"
+ echo ""
+
+ INDEX=$((INDEX + 1))
+ done
+ fi
+
+ # Agent sessions
+ if [[ -v AGENT_SESSIONS[@] ]] && [ ${#AGENT_SESSIONS[@]} -gt 0 ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Spawned Agents (${#AGENT_SESSIONS[@]})"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ local INDEX=1
+ for session_data in "${AGENT_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ echo "### $INDEX. $SESSION_NAME"
+
+ # Get agent metadata
+ METADATA_FILE=$(get_agent_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ TASK=$(jq -r '.task // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ STATUS_=$(jq -r '.status // "running"' "$METADATA_FILE" 2>/dev/null)
+ DIRECTORY=$(jq -r '.directory // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ CREATED=$(jq -r '.created // "unknown"' "$METADATA_FILE" 2>/dev/null)
+
+ echo "- **Agent Type**: $AGENT_TYPE"
+ echo "- **Task**: $TASK"
+ echo "- **Status**: $([ "$STATUS_" = "completed" ] && echo "✅ Completed" || echo "⚙️ Running")"
+ echo "- **Working Directory**: $DIRECTORY"
+ echo "- **Created**: $CREATED"
+
+ # Check for worktree
+ if jq -e '.worktree' "$METADATA_FILE" &>/dev/null; then
+ WORKTREE=$(jq -r '.worktree' "$METADATA_FILE" 2>/dev/null)
+ if [ "$WORKTREE" = "true" ]; then
+ AGENT_BRANCH=$(jq -r '.agent_branch // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ echo "- **Git Worktree**: Yes (branch: $AGENT_BRANCH)"
+ fi
+ fi
+ fi
+
+ echo "- **Attach**: \`tmux attach -t $SESSION_NAME\`"
+ echo "- **Metadata**: \`cat $METADATA_FILE\`"
+ echo ""
+
+ INDEX=$((INDEX + 1))
+ done
+ fi
+
+ # Claude sessions
+ if [[ -v CLAUDE_SESSIONS[@] ]] && [ ${#CLAUDE_SESSIONS[@]} -gt 0 ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Other Sessions (${#CLAUDE_SESSIONS[@]})"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ for session_data in "${CLAUDE_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="Detached"
+ [ "$ATTACHED" = "1" ] && STATUS="⚡ Active (current session)"
+ echo "- $SESSION_NAME: $STATUS"
+ done
+ echo ""
+ fi
+
+ # Running processes summary
+ if [ -n "$RUNNING_PORTS" ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Running Processes Summary"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "| Port | Service | Status |"
+ echo "|------|---------|--------|"
+ for PORT in $RUNNING_PORTS; do
+ echo "| $PORT | Running | ✅ |"
+ done
+ echo ""
+ fi
+
+ # Quick actions
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Quick Actions"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "**List all sessions**:"
+ echo "\`\`\`bash"
+ echo "tmux ls"
+ echo "\`\`\`"
+ echo ""
+ echo "**Attach to session**:"
+ echo "\`\`\`bash"
+ echo "tmux attach -t "
+ echo "\`\`\`"
+ echo ""
+ echo "**Kill session**:"
+ echo "\`\`\`bash"
+ echo "tmux kill-session -t "
+ echo "\`\`\`"
+ echo ""
+}
+
+output_json() {
+ echo "{"
+ echo " \"sessions\": ["
+
+ local FIRST_SESSION=true
+
+ # Dev sessions
+ for session_data in "${DEV_SESSIONS[@]}"; do
+ [ "$FIRST_SESSION" = false ] && echo ","
+ FIRST_SESSION=false
+
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ echo " {"
+ echo " \"name\": \"$SESSION_NAME\","
+ echo " \"type\": \"dev-environment\","
+ echo " \"windows\": $WINDOW_COUNT,"
+ echo " \"panes\": $PANE_COUNT,"
+ echo " \"attached\": $([ "$ATTACHED" = "1" ] && echo "true" || echo "false")"
+
+ # Get metadata if available
+ METADATA_FILE=$(get_dev_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ echo " ,\"metadata_file\": \"$METADATA_FILE\""
+ fi
+
+ echo -n " }"
+ done
+
+ # Agent sessions
+ for session_data in "${AGENT_SESSIONS[@]}"; do
+ [ "$FIRST_SESSION" = false ] && echo ","
+ FIRST_SESSION=false
+
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ echo " {"
+ echo " \"name\": \"$SESSION_NAME\","
+ echo " \"type\": \"spawned-agent\","
+ echo " \"windows\": $WINDOW_COUNT,"
+ echo " \"panes\": $PANE_COUNT"
+
+ # Get agent metadata
+ METADATA_FILE=$(get_agent_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ STATUS_=$(jq -r '.status // "running"' "$METADATA_FILE" 2>/dev/null)
+ echo " ,\"agent_type\": \"$AGENT_TYPE\","
+ echo " \"status\": \"$STATUS_\","
+ echo " \"metadata_file\": \"$METADATA_FILE\""
+ fi
+
+ echo -n " }"
+ done
+
+ echo ""
+ echo " ],"
+ echo " \"summary\": {"
+ echo " \"total_sessions\": $TOTAL_SESSIONS,"
+ echo " \"total_windows\": $TOTAL_WINDOWS,"
+ echo " \"total_panes\": $TOTAL_PANES,"
+ echo " \"dev_sessions\": ${#DEV_SESSIONS[@]},"
+ echo " \"agent_sessions\": ${#AGENT_SESSIONS[@]}"
+ echo " }"
+ echo "}"
+}
+
+# Main output
+case "$OUTPUT_MODE" in
+ compact)
+ output_compact
+ ;;
+ detailed)
+ output_detailed
+ ;;
+ json)
+ output_json
+ ;;
+ *)
+ echo "Unknown output mode: $OUTPUT_MODE"
+ echo "Usage: monitor.sh [compact|detailed|json]"
+ exit 1
+ ;;
+esac
diff --git a/claude-code-4.5/skills/webapp-testing/SKILL.md b/claude-code-4.5/skills/webapp-testing/SKILL.md
index 4726215..a882380 100644
--- a/claude-code-4.5/skills/webapp-testing/SKILL.md
+++ b/claude-code-4.5/skills/webapp-testing/SKILL.md
@@ -38,14 +38,14 @@ To start a server, run `--help` first, then use the helper:
**Single server:**
```bash
-python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py
+python scripts/with_server.py --server "npm run dev" --port 3000 -- python your_automation.py
```
**Multiple servers (e.g., backend + frontend):**
```bash
python scripts/with_server.py \
- --server "cd backend && python server.py" --port 3000 \
- --server "cd frontend && npm run dev" --port 5173 \
+ --server "cd backend && python server.py" --port 8000 \
+ --server "cd frontend && npm run dev" --port 3000 \
-- python your_automation.py
```
@@ -53,10 +53,12 @@ To create an automation script, include only Playwright logic (servers are manag
```python
from playwright.sync_api import sync_playwright
+APP_PORT = 3000 # Match the port from --port argument
+
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
page = browser.new_page()
- page.goto('http://localhost:5173') # Server already running and ready
+ page.goto(f'http://localhost:{APP_PORT}') # Server already running and ready
page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
# ... your automation logic
browser.close()
@@ -88,9 +90,184 @@ with sync_playwright() as p:
- Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs
- Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()`
+## Utility Modules
+
+The skill now includes comprehensive utilities for common testing patterns:
+
+### UI Interactions (`utils/ui_interactions.py`)
+
+Handle common UI patterns automatically:
+
+```python
+from utils.ui_interactions import (
+ dismiss_cookie_banner,
+ dismiss_modal,
+ click_with_header_offset,
+ force_click_if_needed,
+ wait_for_no_overlay,
+ wait_for_stable_dom
+)
+
+# Dismiss cookie consent
+dismiss_cookie_banner(page)
+
+# Close welcome modal
+dismiss_modal(page, modal_identifier="Welcome")
+
+# Click button behind fixed header
+click_with_header_offset(page, 'button#submit', header_height=100)
+
+# Try click with force fallback
+force_click_if_needed(page, 'button#action')
+
+# Wait for loading overlays to disappear
+wait_for_no_overlay(page)
+
+# Wait for DOM to stabilize
+wait_for_stable_dom(page)
+```
+
+### Smart Form Filling (`utils/form_helpers.py`)
+
+Intelligently handle form variations:
+
+```python
+from utils.form_helpers import (
+ SmartFormFiller,
+ handle_multi_step_form,
+ auto_fill_form
+)
+
+# Works with both "Full Name" and "First/Last Name" fields
+filler = SmartFormFiller()
+filler.fill_name_field(page, "Jane Doe")
+filler.fill_email_field(page, "jane@example.com")
+filler.fill_password_fields(page, "SecurePass123!")
+filler.fill_phone_field(page, "+447700900123")
+filler.fill_date_field(page, "1990-01-15", field_hint="birth")
+
+# Auto-fill entire form
+results = auto_fill_form(page, {
+ 'email': 'test@example.com',
+ 'password': 'Pass123!',
+ 'full_name': 'Test User',
+ 'phone': '+447700900123',
+ 'date_of_birth': '1990-01-15'
+})
+
+# Handle multi-step forms
+steps = [
+ {'fields': {'email': 'test@example.com', 'password': 'Pass123!'}, 'checkbox': True},
+ {'fields': {'full_name': 'Test User', 'date_of_birth': '1990-01-15'}},
+ {'complete': True}
+]
+handle_multi_step_form(page, steps)
+```
+
+### Supabase Testing (`utils/supabase.py`)
+
+Database operations for Supabase-based apps:
+
+```python
+from utils.supabase import SupabaseTestClient, quick_cleanup
+
+# Initialize client
+client = SupabaseTestClient(
+ url="https://project.supabase.co",
+ service_key="your-service-role-key",
+ db_password="your-db-password"
+)
+
+# Create test user
+user_id = client.create_user("test@example.com", "password123")
+
+# Create invite code
+client.create_invite_code("TEST2024", code_type="general")
+
+# Bypass email verification
+client.confirm_email(user_id)
+
+# Cleanup after test
+client.cleanup_related_records(user_id)
+client.delete_user(user_id)
+
+# Quick cleanup helper
+quick_cleanup("test@example.com", "db_password", "https://project.supabase.co")
+```
+
+### Advanced Wait Strategies (`utils/wait_strategies.py`)
+
+Better alternatives to simple sleep():
+
+```python
+from utils.wait_strategies import (
+ wait_for_api_call,
+ wait_for_element_stable,
+ smart_navigation_wait,
+ combined_wait
+)
+
+# Wait for specific API response
+response = wait_for_api_call(page, '**/api/profile**')
+
+# Wait for element to stop moving
+wait_for_element_stable(page, '.dropdown-menu', stability_ms=1000)
+
+# Smart navigation with URL check
+page.click('button#login')
+smart_navigation_wait(page, expected_url_pattern='**/dashboard**')
+
+# Comprehensive wait (network + DOM + overlays)
+combined_wait(page)
+```
+
+## Complete Examples
+
+### Multi-Step Registration
+
+See `examples/multi_step_registration.py` for a complete example showing:
+- Database setup (invite codes)
+- Cookie banner dismissal
+- Multi-step form automation
+- Email verification bypass
+- Login flow
+- Dashboard verification
+- Cleanup
+
+Run it:
+```bash
+python examples/multi_step_registration.py
+```
+
+## Using the Webapp-Testing Subagent
+
+A specialized subagent is available for testing automation. Use it to keep your main conversation focused on development:
+
+```
+You: "Use webapp-testing agent to register test@example.com and verify the parent role switch works"
+
+Main Agent: [Launches webapp-testing subagent]
+
+Webapp-Testing Agent: [Runs complete automation, returns results]
+```
+
+**Benefits:**
+- Keeps main context clean
+- Specialized for Playwright automation
+- Access to all skill utilities
+- Automatic screenshot capture
+- Clear result reporting
+
## Reference Files
- **examples/** - Examples showing common patterns:
- `element_discovery.py` - Discovering buttons, links, and inputs on a page
- `static_html_automation.py` - Using file:// URLs for local HTML
- - `console_logging.py` - Capturing console logs during automation
\ No newline at end of file
+ - `console_logging.py` - Capturing console logs during automation
+ - `multi_step_registration.py` - Complete registration flow example (NEW)
+
+- **utils/** - Reusable utility modules (NEW):
+ - `ui_interactions.py` - Cookie banners, modals, overlays, stable waits
+ - `form_helpers.py` - Smart form filling, multi-step automation
+ - `supabase.py` - Database operations for Supabase apps
+ - `wait_strategies.py` - Advanced waiting patterns
\ No newline at end of file
diff --git a/claude-code-4.5/skills/webapp-testing/examples/element_discovery.py b/claude-code-4.5/skills/webapp-testing/examples/element_discovery.py
index 917ba72..8ddc5af 100755
--- a/claude-code-4.5/skills/webapp-testing/examples/element_discovery.py
+++ b/claude-code-4.5/skills/webapp-testing/examples/element_discovery.py
@@ -7,7 +7,7 @@
page = browser.new_page()
# Navigate to page and wait for it to fully load
- page.goto('http://localhost:5173')
+ page.goto('http://localhost:3000') # Replace with your app URL
page.wait_for_load_state('networkidle')
# Discover all buttons on the page
diff --git a/claude-code-4.5/skills/webapp-testing/examples/multi_step_registration.py b/claude-code-4.5/skills/webapp-testing/examples/multi_step_registration.py
new file mode 100644
index 0000000..e960ff6
--- /dev/null
+++ b/claude-code-4.5/skills/webapp-testing/examples/multi_step_registration.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3
+"""
+Multi-Step Registration Example
+
+Demonstrates complete registration flow using all webapp-testing utilities:
+- UI interactions (cookie banners, modals)
+- Smart form filling (handles field variations)
+- Database operations (invite codes, email verification)
+- Advanced wait strategies
+
+This example is based on a real-world React/Supabase app with 3-step registration.
+"""
+
+import sys
+import os
+from playwright.sync_api import sync_playwright
+import time
+
+# Add utils to path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from utils.ui_interactions import dismiss_cookie_banner, dismiss_modal
+from utils.form_helpers import SmartFormFiller, handle_multi_step_form
+from utils.supabase import SupabaseTestClient
+from utils.wait_strategies import combined_wait, smart_navigation_wait
+
+
+def register_user_complete_flow():
+ """
+ Complete multi-step registration with database setup and verification.
+
+ Flow:
+ 1. Create invite code in database
+ 2. Navigate to registration page
+ 3. Fill multi-step form (Code → Credentials → Personal Info → Avatar)
+ 4. Verify email via database
+ 5. Login
+ 6. Verify dashboard access
+ 7. Cleanup (optional)
+ """
+
+ # Configuration - adjust for your app
+ APP_URL = "http://localhost:3000"
+ REGISTER_URL = f"{APP_URL}/register"
+
+ # Database config (adjust for your project)
+ DB_PASSWORD = "your-db-password"
+ SUPABASE_URL = "https://project.supabase.co"
+ SERVICE_KEY = "your-service-role-key"
+
+ # Test user data
+ TEST_EMAIL = "test.user@example.com"
+ TEST_PASSWORD = "TestPass123!"
+ FULL_NAME = "Test User"
+ PHONE = "+447700900123"
+ DATE_OF_BIRTH = "1990-01-15"
+ INVITE_CODE = "TEST2024"
+
+ print("\n" + "="*60)
+ print("MULTI-STEP REGISTRATION AUTOMATION")
+ print("="*60)
+
+ # Step 1: Setup database
+ print("\n[1/8] Setting up database...")
+ db_client = SupabaseTestClient(
+ url=SUPABASE_URL,
+ service_key=SERVICE_KEY,
+ db_password=DB_PASSWORD
+ )
+
+ # Create invite code
+ if db_client.create_invite_code(INVITE_CODE, code_type="general"):
+ print(f" ✓ Created invite code: {INVITE_CODE}")
+ else:
+ print(f" ⚠️ Invite code may already exist")
+
+ # Clean up any existing test user
+ existing_user = db_client.find_user_by_email(TEST_EMAIL)
+ if existing_user:
+ print(f" Cleaning up existing user...")
+ db_client.cleanup_related_records(existing_user)
+ db_client.delete_user(existing_user)
+
+ # Step 2: Start browser automation
+ print("\n[2/8] Starting browser automation...")
+
+ with sync_playwright() as p:
+ browser = p.chromium.launch(headless=False)
+ page = browser.new_page(viewport={'width': 1400, 'height': 1000})
+
+ try:
+ # Step 3: Navigate to registration
+ print("\n[3/8] Navigating to registration page...")
+ page.goto(REGISTER_URL, wait_until='networkidle')
+ time.sleep(2)
+
+ # Handle cookie banner
+ if dismiss_cookie_banner(page):
+ print(" ✓ Dismissed cookie banner")
+
+ page.screenshot(path='/tmp/reg_step1_start.png', full_page=True)
+ print(" ✓ Screenshot: /tmp/reg_step1_start.png")
+
+ # Step 4: Fill multi-step form
+ print("\n[4/8] Filling multi-step registration form...")
+
+ # Define form steps
+ steps = [
+ {
+ 'name': 'Invite Code',
+ 'fields': {'invite_code': INVITE_CODE},
+ 'custom_fill': lambda: page.locator('input').first.fill(INVITE_CODE),
+ 'custom_submit': lambda: page.locator('input').first.press('Enter'),
+ },
+ {
+ 'name': 'Credentials',
+ 'fields': {
+ 'email': TEST_EMAIL,
+ 'password': TEST_PASSWORD,
+ },
+ 'checkbox': True, # Terms of service
+ },
+ {
+ 'name': 'Personal Info',
+ 'fields': {
+ 'full_name': FULL_NAME,
+ 'date_of_birth': DATE_OF_BIRTH,
+ 'phone': PHONE,
+ },
+ },
+ {
+ 'name': 'Avatar Selection',
+ 'complete': True, # Final step with COMPLETE button
+ }
+ ]
+
+ # Process each step
+ filler = SmartFormFiller()
+
+ for i, step in enumerate(steps):
+ print(f"\n Step {i+1}/4: {step['name']}")
+
+ # Custom filling logic for first step (invite code)
+ if 'custom_fill' in step:
+ step['custom_fill']()
+ time.sleep(1)
+
+ if 'custom_submit' in step:
+ step['custom_submit']()
+ else:
+ page.locator('button:has-text("CONTINUE")').first.click()
+
+ time.sleep(4)
+ page.wait_for_load_state('networkidle')
+ time.sleep(2)
+
+ # Standard form filling for other steps
+ elif 'fields' in step:
+ if 'email' in step['fields']:
+ filler.fill_email_field(page, step['fields']['email'])
+ print(" ✓ Email")
+
+ if 'password' in step['fields']:
+ filler.fill_password_fields(page, step['fields']['password'])
+ print(" ✓ Password")
+
+ if 'full_name' in step['fields']:
+ filler.fill_name_field(page, step['fields']['full_name'])
+ print(" ✓ Full Name")
+
+ if 'date_of_birth' in step['fields']:
+ filler.fill_date_field(page, step['fields']['date_of_birth'], field_hint='birth')
+ print(" ✓ Date of Birth")
+
+ if 'phone' in step['fields']:
+ filler.fill_phone_field(page, step['fields']['phone'])
+ print(" ✓ Phone")
+
+ # Check terms checkbox if needed
+ if step.get('checkbox'):
+ page.locator('input[type="checkbox"]').first.check()
+ print(" ✓ Terms accepted")
+
+ time.sleep(1)
+
+ # Click continue
+ page.locator('button:has-text("CONTINUE")').first.click()
+ time.sleep(4)
+ page.wait_for_load_state('networkidle')
+ time.sleep(2)
+
+ # Final step - click COMPLETE
+ elif step.get('complete'):
+ complete_btn = page.locator('button:has-text("COMPLETE")').first
+ complete_btn.click()
+ print(" ✓ Clicked COMPLETE")
+
+ time.sleep(8)
+ page.wait_for_load_state('networkidle')
+ time.sleep(3)
+
+ # Screenshot after each step
+ page.screenshot(path=f'/tmp/reg_step{i+1}_complete.png', full_page=True)
+ print(f" ✓ Screenshot: /tmp/reg_step{i+1}_complete.png")
+
+ print("\n ✓ Multi-step form completed!")
+
+ # Step 5: Handle post-registration
+ print("\n[5/8] Handling post-registration...")
+
+ # Dismiss welcome modal if present
+ if dismiss_modal(page, modal_identifier="Welcome"):
+ print(" ✓ Dismissed welcome modal")
+
+ current_url = page.url
+ print(f" Current URL: {current_url}")
+
+ # Step 6: Verify email via database
+ print("\n[6/8] Verifying email via database...")
+ time.sleep(2) # Brief wait for user to be created in DB
+
+ user_id = db_client.find_user_by_email(TEST_EMAIL)
+ if user_id:
+ print(f" ✓ Found user: {user_id}")
+
+ if db_client.confirm_email(user_id):
+ print(" ✓ Email verified in database")
+ else:
+ print(" ⚠️ Could not verify email")
+ else:
+ print(" ⚠️ User not found in database")
+
+ # Step 7: Login (if not already logged in)
+ print("\n[7/8] Logging in...")
+
+ if 'login' in current_url.lower():
+ print(" Needs login...")
+
+ filler.fill_email_field(page, TEST_EMAIL)
+ filler.fill_password_fields(page, TEST_PASSWORD, confirm=False)
+ time.sleep(1)
+
+ page.locator('button[type="submit"]').first.click()
+ time.sleep(6)
+ page.wait_for_load_state('networkidle')
+ time.sleep(3)
+
+ print(" ✓ Logged in")
+ else:
+ print(" ✓ Already logged in")
+
+ # Step 8: Verify dashboard access
+ print("\n[8/8] Verifying dashboard access...")
+
+ # Navigate to dashboard/perform if not already there
+ if 'perform' not in page.url.lower() and 'dashboard' not in page.url.lower():
+ page.goto(f"{APP_URL}/perform", wait_until='networkidle')
+ time.sleep(3)
+
+ page.screenshot(path='/tmp/reg_final_dashboard.png', full_page=True)
+ print(" ✓ Screenshot: /tmp/reg_final_dashboard.png")
+
+ # Check if we're on the dashboard
+ if 'perform' in page.url.lower() or 'dashboard' in page.url.lower():
+ print(" ✓ Successfully reached dashboard!")
+ else:
+ print(f" ⚠️ Unexpected URL: {page.url}")
+
+ print("\n" + "="*60)
+ print("REGISTRATION COMPLETE!")
+ print("="*60)
+ print(f"\nUser: {TEST_EMAIL}")
+ print(f"Password: {TEST_PASSWORD}")
+ print(f"User ID: {user_id}")
+ print(f"\nScreenshots saved to /tmp/reg_step*.png")
+ print("="*60)
+
+ # Keep browser open for inspection
+ print("\nKeeping browser open for 30 seconds...")
+ time.sleep(30)
+
+ except Exception as e:
+ print(f"\n❌ Error: {e}")
+ import traceback
+ traceback.print_exc()
+ page.screenshot(path='/tmp/reg_error.png', full_page=True)
+ print(" Error screenshot: /tmp/reg_error.png")
+
+ finally:
+ browser.close()
+
+ # Optional cleanup
+ print("\n" + "="*60)
+ print("Cleanup")
+ print("="*60)
+
+ cleanup = input("\nDelete test user? (y/N): ").strip().lower()
+ if cleanup == 'y' and user_id:
+ print("Cleaning up...")
+ db_client.cleanup_related_records(user_id)
+ db_client.delete_user(user_id)
+ print("✓ Test user deleted")
+ else:
+ print("Test user kept for manual testing")
+
+
+if __name__ == '__main__':
+ print("\nMulti-Step Registration Automation Example")
+ print("=" * 60)
+ print("\nBefore running:")
+ print("1. Update configuration variables at the top of the script")
+ print("2. Ensure your app is running (e.g., npm run dev)")
+ print("3. Have database credentials ready")
+ print("\n" + "=" * 60)
+
+ proceed = input("\nProceed with registration? (y/N): ").strip().lower()
+
+ if proceed == 'y':
+ register_user_complete_flow()
+ else:
+ print("\nCancelled.")
diff --git a/claude-code-4.5/skills/webapp-testing/utils/__pycache__/form_helpers.cpython-312.pyc b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/form_helpers.cpython-312.pyc
new file mode 100644
index 0000000..93f6999
Binary files /dev/null and b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/form_helpers.cpython-312.pyc differ
diff --git a/claude-code-4.5/skills/webapp-testing/utils/__pycache__/supabase.cpython-312.pyc b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/supabase.cpython-312.pyc
new file mode 100644
index 0000000..989c929
Binary files /dev/null and b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/supabase.cpython-312.pyc differ
diff --git a/claude-code-4.5/skills/webapp-testing/utils/__pycache__/ui_interactions.cpython-312.pyc b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/ui_interactions.cpython-312.pyc
new file mode 100644
index 0000000..0b547b4
Binary files /dev/null and b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/ui_interactions.cpython-312.pyc differ
diff --git a/claude-code-4.5/skills/webapp-testing/utils/__pycache__/wait_strategies.cpython-312.pyc b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/wait_strategies.cpython-312.pyc
new file mode 100644
index 0000000..2de673e
Binary files /dev/null and b/claude-code-4.5/skills/webapp-testing/utils/__pycache__/wait_strategies.cpython-312.pyc differ
diff --git a/claude-code-4.5/skills/webapp-testing/utils/form_helpers.py b/claude-code-4.5/skills/webapp-testing/utils/form_helpers.py
new file mode 100644
index 0000000..e011f5f
--- /dev/null
+++ b/claude-code-4.5/skills/webapp-testing/utils/form_helpers.py
@@ -0,0 +1,463 @@
+"""
+Smart Form Filling Helpers
+
+Handles common form patterns across web applications:
+- Multi-step forms with validation
+- Dynamic field variations (full name vs first/last name)
+- Retry strategies for flaky selectors
+- Intelligent field detection
+"""
+
+from playwright.sync_api import Page
+from typing import Dict, List, Any, Optional
+import time
+
+
+class SmartFormFiller:
+ """
+ Intelligent form filling that handles variations in field structures.
+
+ Example:
+ ```python
+ filler = SmartFormFiller()
+ filler.fill_name_field(page, "John Doe") # Tries full name or first/last
+ filler.fill_email_field(page, "test@example.com")
+ filler.fill_password_fields(page, "SecurePass123!")
+ ```
+ """
+
+ @staticmethod
+ def fill_name_field(page: Page, full_name: str, timeout: int = 5000) -> bool:
+ """
+ Fill name field(s) - handles both single "Full Name" and separate "First/Last Name" fields.
+
+ Args:
+ page: Playwright Page object
+ full_name: Full name as string (e.g., "John Doe")
+ timeout: Maximum time to wait for fields (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ # Works with both field structures:
+ # - Single field: "Full Name"
+ # - Separate fields: "First Name" and "Last Name"
+ fill_name_field(page, "Jane Smith")
+ ```
+ """
+ # Strategy 1: Try single "Full Name" field
+ full_name_selectors = [
+ 'input[name*="full" i][name*="name" i]',
+ 'input[placeholder*="full name" i]',
+ 'input[placeholder*="name" i]',
+ 'input[id*="fullname" i]',
+ 'input[id*="full-name" i]',
+ ]
+
+ for selector in full_name_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(full_name)
+ return True
+ except:
+ continue
+
+ # Strategy 2: Try separate First/Last Name fields
+ parts = full_name.split(' ', 1)
+ first_name = parts[0] if parts else full_name
+ last_name = parts[1] if len(parts) > 1 else ''
+
+ first_name_selectors = [
+ 'input[name*="first" i][name*="name" i]',
+ 'input[placeholder*="first name" i]',
+ 'input[id*="firstname" i]',
+ 'input[id*="first-name" i]',
+ ]
+
+ last_name_selectors = [
+ 'input[name*="last" i][name*="name" i]',
+ 'input[placeholder*="last name" i]',
+ 'input[id*="lastname" i]',
+ 'input[id*="last-name" i]',
+ ]
+
+ first_filled = False
+ last_filled = False
+
+ # Fill first name
+ for selector in first_name_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(first_name)
+ first_filled = True
+ break
+ except:
+ continue
+
+ # Fill last name
+ for selector in last_name_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(last_name)
+ last_filled = True
+ break
+ except:
+ continue
+
+ return first_filled or last_filled
+
+ @staticmethod
+ def fill_email_field(page: Page, email: str, timeout: int = 5000) -> bool:
+ """
+ Fill email field with multiple selector strategies.
+
+ Args:
+ page: Playwright Page object
+ email: Email address
+ timeout: Maximum time to wait for field (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ email_selectors = [
+ 'input[type="email"]',
+ 'input[name="email" i]',
+ 'input[placeholder*="email" i]',
+ 'input[id*="email" i]',
+ 'input[autocomplete="email"]',
+ ]
+
+ for selector in email_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(email)
+ return True
+ except:
+ continue
+
+ return False
+
+ @staticmethod
+ def fill_password_fields(page: Page, password: str, confirm: bool = True, timeout: int = 5000) -> bool:
+ """
+ Fill password field(s) - handles both single password and password + confirm.
+
+ Args:
+ page: Playwright Page object
+ password: Password string
+ confirm: Whether to also fill confirmation field (default True)
+ timeout: Maximum time to wait for fields (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ password_fields = page.locator('input[type="password"]').all()
+
+ if not password_fields:
+ return False
+
+ # Fill first password field
+ try:
+ password_fields[0].fill(password)
+ except:
+ return False
+
+ # Fill confirmation field if requested and exists
+ if confirm and len(password_fields) > 1:
+ try:
+ password_fields[1].fill(password)
+ except:
+ pass
+
+ return True
+
+ @staticmethod
+ def fill_phone_field(page: Page, phone: str, timeout: int = 5000) -> bool:
+ """
+ Fill phone number field with multiple selector strategies.
+
+ Args:
+ page: Playwright Page object
+ phone: Phone number string
+ timeout: Maximum time to wait for field (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ phone_selectors = [
+ 'input[type="tel"]',
+ 'input[name*="phone" i]',
+ 'input[placeholder*="phone" i]',
+ 'input[id*="phone" i]',
+ 'input[autocomplete="tel"]',
+ ]
+
+ for selector in phone_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(phone)
+ return True
+ except:
+ continue
+
+ return False
+
+ @staticmethod
+ def fill_date_field(page: Page, date_value: str, field_hint: str = None, timeout: int = 5000) -> bool:
+ """
+ Fill date field (handles both date input and text input).
+
+ Args:
+ page: Playwright Page object
+ date_value: Date as string (format: YYYY-MM-DD for date inputs)
+ field_hint: Optional hint about field (e.g., "birth", "start", "end")
+ timeout: Maximum time to wait for field (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ fill_date_field(page, "1990-01-15", field_hint="birth")
+ ```
+ """
+ # Build selectors based on hint
+ date_selectors = ['input[type="date"]']
+
+ if field_hint:
+ date_selectors.extend([
+ f'input[name*="{field_hint}" i]',
+ f'input[placeholder*="{field_hint}" i]',
+ f'input[id*="{field_hint}" i]',
+ ])
+
+ for selector in date_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(date_value)
+ return True
+ except:
+ continue
+
+ return False
+
+
+def fill_with_retry(page: Page, selectors: List[str], value: str, max_attempts: int = 3) -> bool:
+ """
+ Try multiple selectors with retry logic.
+
+ Args:
+ page: Playwright Page object
+ selectors: List of CSS selectors to try
+ value: Value to fill
+ max_attempts: Maximum retry attempts per selector
+
+ Returns:
+ True if any selector succeeded, False otherwise
+
+ Example:
+ ```python
+ selectors = ['input#email', 'input[name="email"]', 'input[type="email"]']
+ fill_with_retry(page, selectors, 'test@example.com')
+ ```
+ """
+ for selector in selectors:
+ for attempt in range(max_attempts):
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(value)
+ time.sleep(0.3)
+ # Verify value was set
+ if field.input_value() == value:
+ return True
+ except:
+ if attempt < max_attempts - 1:
+ time.sleep(0.5)
+ continue
+
+ return False
+
+
+def handle_multi_step_form(page: Page, steps: List[Dict[str, Any]], continue_button_text: str = "CONTINUE") -> bool:
+ """
+ Automate multi-step form completion.
+
+ Args:
+ page: Playwright Page object
+ steps: List of step configurations, each with fields and actions
+ continue_button_text: Text of button to advance steps
+
+ Returns:
+ True if all steps completed successfully, False otherwise
+
+ Example:
+ ```python
+ steps = [
+ {
+ 'fields': {'email': 'test@example.com', 'password': 'Pass123!'},
+ 'checkbox': 'terms', # Optional checkbox to check
+ 'wait_after': 2, # Optional wait time after step
+ },
+ {
+ 'fields': {'full_name': 'John Doe', 'date_of_birth': '1990-01-15'},
+ },
+ {
+ 'complete': True, # Final step, click complete/finish button
+ }
+ ]
+ handle_multi_step_form(page, steps)
+ ```
+ """
+ filler = SmartFormFiller()
+
+ for i, step in enumerate(steps):
+ print(f" Processing step {i+1}/{len(steps)}...")
+
+ # Fill fields in this step
+ if 'fields' in step:
+ for field_type, value in step['fields'].items():
+ if field_type == 'email':
+ filler.fill_email_field(page, value)
+ elif field_type == 'password':
+ filler.fill_password_fields(page, value)
+ elif field_type == 'full_name':
+ filler.fill_name_field(page, value)
+ elif field_type == 'phone':
+ filler.fill_phone_field(page, value)
+ elif field_type.startswith('date'):
+ hint = field_type.replace('date_', '').replace('_', ' ')
+ filler.fill_date_field(page, value, field_hint=hint)
+ else:
+ # Generic field - try to find and fill
+ print(f" Warning: Unknown field type '{field_type}'")
+
+ # Check checkbox if specified
+ if 'checkbox' in step:
+ try:
+ checkbox = page.locator('input[type="checkbox"]').first
+ checkbox.check()
+ except:
+ print(f" Warning: Could not check checkbox")
+
+ # Wait if specified
+ if 'wait_after' in step:
+ time.sleep(step['wait_after'])
+ else:
+ time.sleep(1)
+
+ # Click continue/submit button
+ if i < len(steps) - 1: # Not the last step
+ button_selectors = [
+ f'button:has-text("{continue_button_text}")',
+ 'button[type="submit"]',
+ 'button:has-text("Next")',
+ 'button:has-text("Continue")',
+ ]
+
+ clicked = False
+ for selector in button_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=2000):
+ button.click()
+ clicked = True
+ break
+ except:
+ continue
+
+ if not clicked:
+ print(f" Warning: Could not find continue button for step {i+1}")
+ return False
+
+ # Wait for next step to load
+ page.wait_for_load_state('networkidle')
+ time.sleep(2)
+
+ else: # Last step
+ if step.get('complete', False):
+ complete_selectors = [
+ 'button:has-text("COMPLETE")',
+ 'button:has-text("Complete")',
+ 'button:has-text("FINISH")',
+ 'button:has-text("Finish")',
+ 'button:has-text("SUBMIT")',
+ 'button:has-text("Submit")',
+ 'button[type="submit"]',
+ ]
+
+ for selector in complete_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=2000):
+ button.click()
+ page.wait_for_load_state('networkidle')
+ time.sleep(3)
+ return True
+ except:
+ continue
+
+ print(" Warning: Could not find completion button")
+ return False
+
+ return True
+
+
+def auto_fill_form(page: Page, field_mapping: Dict[str, str]) -> Dict[str, bool]:
+ """
+ Automatically fill a form based on field mapping.
+
+ Intelligently detects field types and uses appropriate filling strategies.
+
+ Args:
+ page: Playwright Page object
+ field_mapping: Dictionary mapping field types to values
+
+ Returns:
+ Dictionary with results for each field (True = filled, False = failed)
+
+ Example:
+ ```python
+ results = auto_fill_form(page, {
+ 'email': 'test@example.com',
+ 'password': 'SecurePass123!',
+ 'full_name': 'Jane Doe',
+ 'phone': '+447700900123',
+ 'date_of_birth': '1990-01-15',
+ })
+ print(f"Email filled: {results['email']}")
+ ```
+ """
+ filler = SmartFormFiller()
+ results = {}
+
+ for field_type, value in field_mapping.items():
+ if field_type == 'email':
+ results[field_type] = filler.fill_email_field(page, value)
+ elif field_type == 'password':
+ results[field_type] = filler.fill_password_fields(page, value)
+ elif 'name' in field_type.lower():
+ results[field_type] = filler.fill_name_field(page, value)
+ elif 'phone' in field_type.lower():
+ results[field_type] = filler.fill_phone_field(page, value)
+ elif 'date' in field_type.lower():
+ hint = field_type.replace('date_of_', '').replace('_', ' ')
+ results[field_type] = filler.fill_date_field(page, value, field_hint=hint)
+ else:
+ # Try generic fill
+ try:
+ field = page.locator(f'input[name="{field_type}"]').first
+ field.fill(value)
+ results[field_type] = True
+ except:
+ results[field_type] = False
+
+ return results
diff --git a/claude-code-4.5/skills/webapp-testing/utils/supabase.py b/claude-code-4.5/skills/webapp-testing/utils/supabase.py
new file mode 100644
index 0000000..ecceac2
--- /dev/null
+++ b/claude-code-4.5/skills/webapp-testing/utils/supabase.py
@@ -0,0 +1,353 @@
+"""
+Supabase Test Utilities
+
+Generic database helpers for testing with Supabase.
+Supports user management, email verification, and test data cleanup.
+"""
+
+import subprocess
+import json
+from typing import Dict, List, Optional, Any
+
+
+class SupabaseTestClient:
+ """
+ Generic Supabase test client for database operations during testing.
+
+ Example:
+ ```python
+ client = SupabaseTestClient(
+ url="https://project.supabase.co",
+ service_key="your-service-role-key",
+ db_password="your-db-password"
+ )
+
+ # Create test user
+ user_id = client.create_user("test@example.com", "password123")
+
+ # Verify email (bypass email sending)
+ client.confirm_email(user_id)
+
+ # Cleanup after test
+ client.delete_user(user_id)
+ ```
+ """
+
+ def __init__(self, url: str, service_key: str, db_password: str = None, db_host: str = None):
+ """
+ Initialize Supabase test client.
+
+ Args:
+ url: Supabase project URL (e.g., "https://project.supabase.co")
+ service_key: Service role key for admin operations
+ db_password: Database password for direct SQL operations
+ db_host: Database host (if different from default)
+ """
+ self.url = url.rstrip('/')
+ self.service_key = service_key
+ self.db_password = db_password
+
+ # Extract DB host from URL if not provided
+ if not db_host:
+ # Convert https://abc123.supabase.co to db.abc123.supabase.co
+ project_ref = url.split('//')[1].split('.')[0]
+ self.db_host = f"db.{project_ref}.supabase.co"
+ else:
+ self.db_host = db_host
+
+ def _run_sql(self, sql: str) -> Dict[str, Any]:
+ """
+ Execute SQL directly against the database.
+
+ Args:
+ sql: SQL query to execute
+
+ Returns:
+ Dictionary with 'success', 'output', 'error' keys
+ """
+ if not self.db_password:
+ return {'success': False, 'error': 'Database password not provided'}
+
+ try:
+ result = subprocess.run(
+ [
+ 'psql',
+ '-h', self.db_host,
+ '-p', '5432',
+ '-U', 'postgres',
+ '-c', sql,
+ '-t', # Tuples only
+ '-A', # Unaligned output
+ ],
+ env={'PGPASSWORD': self.db_password},
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+
+ return {
+ 'success': result.returncode == 0,
+ 'output': result.stdout.strip(),
+ 'error': result.stderr.strip() if result.returncode != 0 else None
+ }
+ except Exception as e:
+ return {'success': False, 'error': str(e)}
+
+ def create_user(self, email: str, password: str, metadata: Dict = None) -> Optional[str]:
+ """
+ Create a test user via Auth Admin API.
+
+ Args:
+ email: User email
+ password: User password
+ metadata: Optional user metadata
+
+ Returns:
+ User ID if successful, None otherwise
+
+ Example:
+ ```python
+ user_id = client.create_user(
+ "test@example.com",
+ "SecurePass123!",
+ metadata={"full_name": "Test User"}
+ )
+ ```
+ """
+ import requests
+
+ payload = {
+ 'email': email,
+ 'password': password,
+ 'email_confirm': True
+ }
+
+ if metadata:
+ payload['user_metadata'] = metadata
+
+ try:
+ response = requests.post(
+ f"{self.url}/auth/v1/admin/users",
+ headers={
+ 'Authorization': f'Bearer {self.service_key}',
+ 'apikey': self.service_key,
+ 'Content-Type': 'application/json'
+ },
+ json=payload,
+ timeout=10
+ )
+
+ if response.ok:
+ return response.json().get('id')
+ else:
+ print(f"Error creating user: {response.text}")
+ return None
+ except Exception as e:
+ print(f"Exception creating user: {e}")
+ return None
+
+ def confirm_email(self, user_id: str = None, email: str = None) -> bool:
+ """
+ Confirm user email (bypass email verification for testing).
+
+ Args:
+ user_id: User ID (if known)
+ email: User email (alternative to user_id)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ # By user ID
+ client.confirm_email(user_id="abc-123")
+
+ # Or by email
+ client.confirm_email(email="test@example.com")
+ ```
+ """
+ if user_id:
+ sql = f"UPDATE auth.users SET email_confirmed_at = NOW() WHERE id = '{user_id}';"
+ elif email:
+ sql = f"UPDATE auth.users SET email_confirmed_at = NOW() WHERE email = '{email}';"
+ else:
+ return False
+
+ result = self._run_sql(sql)
+ return result['success']
+
+ def delete_user(self, user_id: str = None, email: str = None) -> bool:
+ """
+ Delete a test user and related data.
+
+ Args:
+ user_id: User ID
+ email: User email (alternative to user_id)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ client.delete_user(email="test@example.com")
+ ```
+ """
+ # Get user ID if email provided
+ if email and not user_id:
+ result = self._run_sql(f"SELECT id FROM auth.users WHERE email = '{email}';")
+ if result['success'] and result['output']:
+ user_id = result['output'].strip()
+ else:
+ return False
+
+ if not user_id:
+ return False
+
+ # Delete from profiles first (foreign key)
+ self._run_sql(f"DELETE FROM public.profiles WHERE id = '{user_id}';")
+
+ # Delete from auth.users
+ result = self._run_sql(f"DELETE FROM auth.users WHERE id = '{user_id}';")
+
+ return result['success']
+
+ def cleanup_related_records(self, user_id: str, tables: List[str] = None) -> Dict[str, bool]:
+ """
+ Clean up user-related records from multiple tables.
+
+ Args:
+ user_id: User ID
+ tables: List of tables to clean (defaults to common tables)
+
+ Returns:
+ Dictionary mapping table names to cleanup success status
+
+ Example:
+ ```python
+ results = client.cleanup_related_records(
+ user_id="abc-123",
+ tables=["profiles", "team_members", "coach_verification_requests"]
+ )
+ ```
+ """
+ if not tables:
+ tables = [
+ 'pending_profiles',
+ 'coach_verification_requests',
+ 'team_members',
+ 'team_join_requests',
+ 'profiles'
+ ]
+
+ results = {}
+
+ for table in tables:
+ # Try both user_id and id columns
+ sql = f"DELETE FROM public.{table} WHERE user_id = '{user_id}' OR id = '{user_id}';"
+ result = self._run_sql(sql)
+ results[table] = result['success']
+
+ return results
+
+ def create_invite_code(self, code: str, code_type: str = 'general', max_uses: int = 999) -> bool:
+ """
+ Create an invite code for testing.
+
+ Args:
+ code: Invite code string
+ code_type: Type of code (e.g., 'general', 'team_join')
+ max_uses: Maximum number of uses
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ client.create_invite_code("TEST2024", code_type="general")
+ ```
+ """
+ sql = f"""
+ INSERT INTO public.invite_codes (code, code_type, is_valid, max_uses, expires_at)
+ VALUES ('{code}', '{code_type}', true, {max_uses}, NOW() + INTERVAL '30 days')
+ ON CONFLICT (code) DO UPDATE SET is_valid=true, max_uses={max_uses}, use_count=0;
+ """
+
+ result = self._run_sql(sql)
+ return result['success']
+
+ def find_user_by_email(self, email: str) -> Optional[str]:
+ """
+ Find user ID by email address.
+
+ Args:
+ email: User email
+
+ Returns:
+ User ID if found, None otherwise
+ """
+ sql = f"SELECT id FROM auth.users WHERE email = '{email}';"
+ result = self._run_sql(sql)
+
+ if result['success'] and result['output']:
+ return result['output'].strip()
+ return None
+
+ def get_user_privileges(self, user_id: str) -> Optional[List[str]]:
+ """
+ Get user's privilege array.
+
+ Args:
+ user_id: User ID
+
+ Returns:
+ List of privileges if found, None otherwise
+ """
+ sql = f"SELECT privileges FROM public.profiles WHERE id = '{user_id}';"
+ result = self._run_sql(sql)
+
+ if result['success'] and result['output']:
+ # Parse PostgreSQL array format
+ privileges_str = result['output'].strip('{}')
+ return [p.strip() for p in privileges_str.split(',')]
+ return None
+
+
+def quick_cleanup(email: str, db_password: str, project_url: str) -> bool:
+ """
+ Quick cleanup helper - delete user and all related data.
+
+ Args:
+ email: User email to delete
+ db_password: Database password
+ project_url: Supabase project URL
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ from utils.supabase import quick_cleanup
+
+ # Clean up test user
+ quick_cleanup(
+ "test@example.com",
+ "db_password",
+ "https://project.supabase.co"
+ )
+ ```
+ """
+ client = SupabaseTestClient(
+ url=project_url,
+ service_key="", # Not needed for SQL operations
+ db_password=db_password
+ )
+
+ user_id = client.find_user_by_email(email)
+ if not user_id:
+ return True # Already deleted
+
+ # Clean up all related tables
+ client.cleanup_related_records(user_id)
+
+ # Delete user
+ return client.delete_user(user_id)
diff --git a/claude-code-4.5/skills/webapp-testing/utils/ui_interactions.py b/claude-code-4.5/skills/webapp-testing/utils/ui_interactions.py
new file mode 100644
index 0000000..1066edf
--- /dev/null
+++ b/claude-code-4.5/skills/webapp-testing/utils/ui_interactions.py
@@ -0,0 +1,382 @@
+"""
+UI Interaction Helpers for Web Automation
+
+Common UI patterns that appear across many web applications:
+- Cookie consent banners
+- Modal dialogs
+- Loading overlays
+- Welcome tours/onboarding
+- Fixed headers blocking clicks
+"""
+
+from playwright.sync_api import Page
+import time
+
+
+def dismiss_cookie_banner(page: Page, timeout: int = 3000) -> bool:
+ """
+ Detect and dismiss cookie consent banners.
+
+ Tries common patterns:
+ - "Accept" / "Accept All" / "OK" buttons
+ - "I Agree" / "Got it" buttons
+ - Cookie banner containers
+
+ Args:
+ page: Playwright Page object
+ timeout: Maximum time to wait for banner (milliseconds)
+
+ Returns:
+ True if banner was found and dismissed, False otherwise
+
+ Example:
+ ```python
+ page.goto('https://example.com')
+ if dismiss_cookie_banner(page):
+ print("Cookie banner dismissed")
+ ```
+ """
+ cookie_button_selectors = [
+ 'button:has-text("Accept")',
+ 'button:has-text("Accept All")',
+ 'button:has-text("Accept all")',
+ 'button:has-text("I Agree")',
+ 'button:has-text("I agree")',
+ 'button:has-text("OK")',
+ 'button:has-text("Got it")',
+ 'button:has-text("Allow")',
+ 'button:has-text("Allow all")',
+ '[data-testid="cookie-accept"]',
+ '[data-testid="accept-cookies"]',
+ '[id*="cookie-accept" i]',
+ '[id*="accept-cookie" i]',
+ '[class*="cookie-accept" i]',
+ ]
+
+ for selector in cookie_button_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=timeout):
+ button.click()
+ time.sleep(0.5) # Brief wait for banner to disappear
+ return True
+ except:
+ continue
+
+ return False
+
+
+def dismiss_modal(page: Page, modal_identifier: str = None, timeout: int = 2000) -> bool:
+ """
+ Close modal dialogs with multiple fallback strategies.
+
+ Strategies:
+ 1. If identifier provided, close that specific modal
+ 2. Click close button (X, Close, Cancel, etc.)
+ 3. Press Escape key
+ 4. Click backdrop/overlay
+
+ Args:
+ page: Playwright Page object
+ modal_identifier: Optional - specific text in modal to identify it
+ timeout: Maximum time to wait for modal (milliseconds)
+
+ Returns:
+ True if modal was found and closed, False otherwise
+
+ Example:
+ ```python
+ # Close any modal
+ dismiss_modal(page)
+
+ # Close specific "Welcome" modal
+ dismiss_modal(page, modal_identifier="Welcome")
+ ```
+ """
+ # If specific modal identifier provided, wait for it first
+ if modal_identifier:
+ try:
+ modal = page.locator(f'[role="dialog"]:has-text("{modal_identifier}"), dialog:has-text("{modal_identifier}")').first
+ if not modal.is_visible(timeout=timeout):
+ return False
+ except:
+ return False
+
+ # Strategy 1: Click close button
+ close_button_selectors = [
+ 'button:has-text("Close")',
+ 'button:has-text("×")',
+ 'button:has-text("X")',
+ 'button:has-text("Cancel")',
+ 'button:has-text("GOT IT")',
+ 'button:has-text("Got it")',
+ 'button:has-text("OK")',
+ 'button:has-text("Dismiss")',
+ '[aria-label="Close"]',
+ '[aria-label="close"]',
+ '[data-testid="close-modal"]',
+ '[class*="close" i]',
+ '[class*="dismiss" i]',
+ ]
+
+ for selector in close_button_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=500):
+ button.click()
+ time.sleep(0.5)
+ return True
+ except:
+ continue
+
+ # Strategy 2: Press Escape key
+ try:
+ page.keyboard.press('Escape')
+ time.sleep(0.5)
+ # Check if modal is gone
+ modals = page.locator('[role="dialog"], dialog').all()
+ if all(not m.is_visible() for m in modals):
+ return True
+ except:
+ pass
+
+ # Strategy 3: Click backdrop (if exists and clickable)
+ try:
+ backdrop = page.locator('[class*="backdrop" i], [class*="overlay" i]').first
+ if backdrop.is_visible(timeout=500):
+ backdrop.click(position={'x': 10, 'y': 10}) # Click corner, not center
+ time.sleep(0.5)
+ return True
+ except:
+ pass
+
+ return False
+
+
+def click_with_header_offset(page: Page, selector: str, header_height: int = 80, force: bool = False):
+ """
+ Click an element while accounting for fixed headers that might block it.
+
+ Scrolls the element into view with an offset to avoid fixed headers,
+ then clicks it.
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element to click
+ header_height: Height of fixed header in pixels (default 80)
+ force: Whether to use force click if normal click fails
+
+ Example:
+ ```python
+ # Click button that might be behind a fixed header
+ click_with_header_offset(page, 'button#submit', header_height=100)
+ ```
+ """
+ element = page.locator(selector).first
+
+ # Scroll element into view with offset
+ element.evaluate(f'el => el.scrollIntoView({{ block: "center", inline: "nearest" }})')
+ page.evaluate(f'window.scrollBy(0, -{header_height})')
+ time.sleep(0.3) # Brief wait for scroll to complete
+
+ try:
+ element.click()
+ except Exception as e:
+ if force:
+ element.click(force=True)
+ else:
+ raise e
+
+
+def force_click_if_needed(page: Page, selector: str, timeout: int = 5000) -> bool:
+ """
+ Try normal click first, use force click if it fails (e.g., due to overlays).
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element to click
+ timeout: Maximum time to wait for element (milliseconds)
+
+ Returns:
+ True if click succeeded (normal or forced), False otherwise
+
+ Example:
+ ```python
+ # Try to click, handling potential overlays
+ if force_click_if_needed(page, 'button#submit'):
+ print("Button clicked successfully")
+ ```
+ """
+ try:
+ element = page.locator(selector).first
+ if not element.is_visible(timeout=timeout):
+ return False
+
+ # Try normal click first
+ try:
+ element.click(timeout=timeout)
+ return True
+ except:
+ # Fall back to force click
+ element.click(force=True)
+ return True
+ except:
+ return False
+
+
+def wait_for_no_overlay(page: Page, max_wait_seconds: int = 10) -> bool:
+ """
+ Wait for loading overlays/spinners to disappear.
+
+ Looks for common loading overlay patterns and waits until they're gone.
+
+ Args:
+ page: Playwright Page object
+ max_wait_seconds: Maximum time to wait (seconds)
+
+ Returns:
+ True if overlays disappeared, False if timeout
+
+ Example:
+ ```python
+ page.click('button#submit')
+ wait_for_no_overlay(page) # Wait for loading to complete
+ ```
+ """
+ overlay_selectors = [
+ '[class*="loading" i]',
+ '[class*="spinner" i]',
+ '[class*="overlay" i]',
+ '[class*="backdrop" i]',
+ '[data-loading="true"]',
+ '[aria-busy="true"]',
+ '.loader',
+ '.loading',
+ '#loading',
+ ]
+
+ start_time = time.time()
+
+ while time.time() - start_time < max_wait_seconds:
+ all_hidden = True
+
+ for selector in overlay_selectors:
+ try:
+ overlays = page.locator(selector).all()
+ for overlay in overlays:
+ if overlay.is_visible():
+ all_hidden = False
+ break
+ except:
+ continue
+
+ if not all_hidden:
+ break
+
+ if all_hidden:
+ return True
+
+ time.sleep(0.5)
+
+ return False
+
+
+def handle_welcome_tour(page: Page, skip_button_text: str = "Skip") -> bool:
+ """
+ Automatically skip onboarding tours or welcome wizards.
+
+ Looks for and clicks "Skip", "Skip Tour", "Close", "Maybe Later" buttons.
+
+ Args:
+ page: Playwright Page object
+ skip_button_text: Text to look for in skip buttons (default "Skip")
+
+ Returns:
+ True if tour was skipped, False if no tour found
+
+ Example:
+ ```python
+ page.goto('https://app.example.com')
+ handle_welcome_tour(page) # Skip any onboarding tour
+ ```
+ """
+ skip_selectors = [
+ f'button:has-text("{skip_button_text}")',
+ 'button:has-text("Skip Tour")',
+ 'button:has-text("Maybe Later")',
+ 'button:has-text("No Thanks")',
+ 'button:has-text("Close Tour")',
+ '[data-testid="skip-tour"]',
+ '[data-testid="close-tour"]',
+ '[aria-label="Skip tour"]',
+ '[aria-label="Close tour"]',
+ ]
+
+ for selector in skip_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=2000):
+ button.click()
+ time.sleep(0.5)
+ return True
+ except:
+ continue
+
+ return False
+
+
+def wait_for_stable_dom(page: Page, stability_duration_ms: int = 1000, max_wait_seconds: int = 10) -> bool:
+ """
+ Wait for the DOM to stop changing (useful for dynamic content loading).
+
+ Monitors for DOM mutations and waits until no changes occur for the specified duration.
+
+ Args:
+ page: Playwright Page object
+ stability_duration_ms: Duration of no changes to consider stable (milliseconds)
+ max_wait_seconds: Maximum time to wait (seconds)
+
+ Returns:
+ True if DOM stabilized, False if timeout
+
+ Example:
+ ```python
+ page.goto('https://app.example.com')
+ wait_for_stable_dom(page) # Wait for all dynamic content to load
+ ```
+ """
+ # Inject mutation observer script
+ script = f"""
+ new Promise((resolve) => {{
+ let lastMutation = Date.now();
+ const observer = new MutationObserver(() => {{
+ lastMutation = Date.now();
+ }});
+
+ observer.observe(document.body, {{
+ childList: true,
+ subtree: true,
+ attributes: true
+ }});
+
+ const checkStability = () => {{
+ if (Date.now() - lastMutation >= {stability_duration_ms}) {{
+ observer.disconnect();
+ resolve(true);
+ }} else if (Date.now() - lastMutation > {max_wait_seconds * 1000}) {{
+ observer.disconnect();
+ resolve(false);
+ }} else {{
+ setTimeout(checkStability, 100);
+ }}
+ }};
+
+ setTimeout(checkStability, {stability_duration_ms});
+ }})
+ """
+
+ try:
+ result = page.evaluate(script)
+ return result
+ except:
+ return False
diff --git a/claude-code-4.5/skills/webapp-testing/utils/wait_strategies.py b/claude-code-4.5/skills/webapp-testing/utils/wait_strategies.py
new file mode 100644
index 0000000..f92d236
--- /dev/null
+++ b/claude-code-4.5/skills/webapp-testing/utils/wait_strategies.py
@@ -0,0 +1,312 @@
+"""
+Advanced Wait Strategies for Reliable Web Automation
+
+Better alternatives to simple sleep() or networkidle for dynamic web applications.
+"""
+
+from playwright.sync_api import Page
+import time
+from typing import Callable, Optional, Any
+
+
+def wait_for_api_call(page: Page, url_pattern: str, timeout_seconds: int = 10) -> Optional[Any]:
+ """
+ Wait for a specific API call to complete and return its response.
+
+ Args:
+ page: Playwright Page object
+ url_pattern: URL pattern to match (can include wildcards)
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ Response data if call completed, None if timeout
+
+ Example:
+ ```python
+ # Wait for user profile API call
+ response = wait_for_api_call(page, '**/api/profile**')
+ if response:
+ print(f"Profile loaded: {response}")
+ ```
+ """
+ response_data = {'data': None, 'completed': False}
+
+ def handle_response(response):
+ if url_pattern.replace('**', '') in response.url:
+ try:
+ response_data['data'] = response.json()
+ response_data['completed'] = True
+ except:
+ response_data['completed'] = True
+
+ page.on('response', handle_response)
+
+ start_time = time.time()
+ while not response_data['completed'] and (time.time() - start_time) < timeout_seconds:
+ time.sleep(0.1)
+
+ page.remove_listener('response', handle_response)
+
+ return response_data['data']
+
+
+def wait_for_element_stable(page: Page, selector: str, stability_ms: int = 1000, timeout_seconds: int = 10) -> bool:
+ """
+ Wait for an element's position to stabilize (stop moving/changing).
+
+ Useful for elements that animate or shift due to dynamic content loading.
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element
+ stability_ms: Duration element must remain stable (milliseconds)
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if element stabilized, False if timeout
+
+ Example:
+ ```python
+ # Wait for dropdown menu to finish animating
+ wait_for_element_stable(page, '.dropdown-menu', stability_ms=500)
+ ```
+ """
+ try:
+ element = page.locator(selector).first
+
+ script = f"""
+ (element, stabilityMs) => {{
+ return new Promise((resolve) => {{
+ let lastRect = element.getBoundingClientRect();
+ let lastChange = Date.now();
+
+ const checkStability = () => {{
+ const currentRect = element.getBoundingClientRect();
+
+ if (currentRect.top !== lastRect.top ||
+ currentRect.left !== lastRect.left ||
+ currentRect.width !== lastRect.width ||
+ currentRect.height !== lastRect.height) {{
+ lastChange = Date.now();
+ lastRect = currentRect;
+ }}
+
+ if (Date.now() - lastChange >= stabilityMs) {{
+ resolve(true);
+ }} else if (Date.now() - lastChange < {timeout_seconds * 1000}) {{
+ setTimeout(checkStability, 50);
+ }} else {{
+ resolve(false);
+ }}
+ }};
+
+ setTimeout(checkStability, stabilityMs);
+ }});
+ }}
+ """
+
+ result = element.evaluate(script, stability_ms)
+ return result
+ except:
+ return False
+
+
+def wait_with_retry(page: Page, condition_fn: Callable[[], bool], max_retries: int = 5, backoff_seconds: float = 0.5) -> bool:
+ """
+ Wait for a condition with exponential backoff retry.
+
+ Args:
+ page: Playwright Page object
+ condition_fn: Function that returns True when condition is met
+ max_retries: Maximum number of retry attempts
+ backoff_seconds: Initial backoff duration (doubles each retry)
+
+ Returns:
+ True if condition met, False if all retries exhausted
+
+ Example:
+ ```python
+ # Wait for specific element to appear with retry
+ def check_dashboard():
+ return page.locator('#dashboard').is_visible()
+
+ if wait_with_retry(page, check_dashboard):
+ print("Dashboard loaded!")
+ ```
+ """
+ wait_time = backoff_seconds
+
+ for attempt in range(max_retries):
+ try:
+ if condition_fn():
+ return True
+ except:
+ pass
+
+ if attempt < max_retries - 1:
+ time.sleep(wait_time)
+ wait_time *= 2 # Exponential backoff
+
+ return False
+
+
+def smart_navigation_wait(page: Page, expected_url_pattern: str = None, timeout_seconds: int = 10) -> bool:
+ """
+ Comprehensive wait strategy after navigation/interaction.
+
+ Combines multiple strategies:
+ 1. Network idle
+ 2. DOM stability
+ 3. URL pattern match (if provided)
+
+ Args:
+ page: Playwright Page object
+ expected_url_pattern: Optional URL pattern to wait for
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if all conditions met, False if timeout
+
+ Example:
+ ```python
+ page.click('button#login')
+ smart_navigation_wait(page, expected_url_pattern='**/dashboard**')
+ ```
+ """
+ start_time = time.time()
+
+ # Step 1: Wait for network idle
+ try:
+ page.wait_for_load_state('networkidle', timeout=timeout_seconds * 1000)
+ except:
+ pass
+
+ # Step 2: Check URL if pattern provided
+ if expected_url_pattern:
+ while (time.time() - start_time) < timeout_seconds:
+ current_url = page.url
+ pattern = expected_url_pattern.replace('**', '')
+ if pattern in current_url:
+ break
+ time.sleep(0.5)
+ else:
+ return False
+
+ # Step 3: Brief wait for DOM stability
+ time.sleep(1)
+
+ return True
+
+
+def wait_for_data_load(page: Page, data_attribute: str = 'data-loaded', timeout_seconds: int = 10) -> bool:
+ """
+ Wait for data-loading attribute to indicate completion.
+
+ Args:
+ page: Playwright Page object
+ data_attribute: Data attribute to check (e.g., 'data-loaded')
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if data loaded, False if timeout
+
+ Example:
+ ```python
+ # Wait for element with data-loaded="true"
+ wait_for_data_load(page, data_attribute='data-loaded')
+ ```
+ """
+ start_time = time.time()
+
+ while (time.time() - start_time) < timeout_seconds:
+ try:
+ elements = page.locator(f'[{data_attribute}="true"]').all()
+ if elements:
+ return True
+ except:
+ pass
+
+ time.sleep(0.3)
+
+ return False
+
+
+def wait_until_no_element(page: Page, selector: str, timeout_seconds: int = 10) -> bool:
+ """
+ Wait until an element is no longer visible (e.g., loading spinner disappears).
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if element disappeared, False if still visible after timeout
+
+ Example:
+ ```python
+ # Wait for loading spinner to disappear
+ wait_until_no_element(page, '.loading-spinner')
+ ```
+ """
+ start_time = time.time()
+
+ while (time.time() - start_time) < timeout_seconds:
+ try:
+ element = page.locator(selector).first
+ if not element.is_visible(timeout=500):
+ return True
+ except:
+ return True # Element not found = disappeared
+
+ time.sleep(0.3)
+
+ return False
+
+
+def combined_wait(page: Page, timeout_seconds: int = 10) -> bool:
+ """
+ Comprehensive wait combining multiple strategies for maximum reliability.
+
+ Uses:
+ 1. Network idle
+ 2. No visible loading indicators
+ 3. DOM stability
+ 4. Brief settling time
+
+ Args:
+ page: Playwright Page object
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if all conditions met, False if timeout
+
+ Example:
+ ```python
+ page.click('button#submit')
+ combined_wait(page) # Wait for everything to settle
+ ```
+ """
+ start_time = time.time()
+
+ # Network idle
+ try:
+ page.wait_for_load_state('networkidle', timeout=timeout_seconds * 1000)
+ except:
+ pass
+
+ # Wait for common loading indicators to disappear
+ loading_selectors = [
+ '.loading',
+ '.spinner',
+ '[data-loading="true"]',
+ '[aria-busy="true"]',
+ ]
+
+ for selector in loading_selectors:
+ wait_until_no_element(page, selector, timeout_seconds=3)
+
+ # Final settling time
+ time.sleep(1)
+
+ return (time.time() - start_time) < timeout_seconds
diff --git a/claude-code-4.5/utils/git-worktree-utils.sh b/claude-code-4.5/utils/git-worktree-utils.sh
new file mode 100755
index 0000000..3e9b9f8
--- /dev/null
+++ b/claude-code-4.5/utils/git-worktree-utils.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+
+# ABOUTME: Git worktree utilities for agent workspace isolation
+
+set -euo pipefail
+
+# Create agent worktree with isolated branch
+create_agent_worktree() {
+ local AGENT_ID=$1
+ local BASE_BRANCH=${2:-$(git branch --show-current)}
+ local TASK_SLUG=${3:-""}
+
+ # Build directory name with optional task slug
+ if [ -n "$TASK_SLUG" ]; then
+ local WORKTREE_DIR="worktrees/agent-${AGENT_ID}-${TASK_SLUG}"
+ else
+ local WORKTREE_DIR="worktrees/agent-${AGENT_ID}"
+ fi
+
+ local BRANCH_NAME="agent/agent-${AGENT_ID}"
+
+ # Create worktrees directory if needed
+ mkdir -p worktrees
+
+ # Create worktree with new branch (redirect git output to stderr)
+ git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "$BASE_BRANCH" >&2
+
+ # Echo only the directory path to stdout
+ echo "$WORKTREE_DIR"
+}
+
+# Remove agent worktree
+cleanup_agent_worktree() {
+ local AGENT_ID=$1
+ local FORCE=${2:-false}
+
+ # Find worktree directory (may have task slug suffix)
+ local WORKTREE_DIR=$(find worktrees -type d -name "agent-${AGENT_ID}*" 2>/dev/null | head -1)
+ local BRANCH_NAME="agent/agent-${AGENT_ID}"
+
+ if [ -z "$WORKTREE_DIR" ] || [ ! -d "$WORKTREE_DIR" ]; then
+ echo "❌ Worktree not found for agent: $AGENT_ID"
+ return 1
+ fi
+
+ # Check for uncommitted changes
+ if ! git -C "$WORKTREE_DIR" diff --quiet 2>/dev/null; then
+ if [ "$FORCE" = false ]; then
+ echo "⚠️ Worktree has uncommitted changes. Use --force to remove anyway."
+ return 1
+ fi
+ fi
+
+ # Remove worktree
+ git worktree remove "$WORKTREE_DIR" $( [ "$FORCE" = true ] && echo "--force" )
+
+ # Delete branch (only if merged or forced)
+ git branch -d "$BRANCH_NAME" 2>/dev/null || \
+ ( [ "$FORCE" = true ] && git branch -D "$BRANCH_NAME" )
+}
+
+# List all agent worktrees
+list_agent_worktrees() {
+ git worktree list | grep "worktrees/agent-" || echo "No agent worktrees found"
+}
+
+# Merge agent work into current branch
+merge_agent_work() {
+ local AGENT_ID=$1
+ local BRANCH_NAME="agent/agent-${AGENT_ID}"
+
+ if ! git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
+ echo "❌ Branch not found: $BRANCH_NAME"
+ return 1
+ fi
+
+ git merge "$BRANCH_NAME"
+}
+
+# Check if worktree exists
+worktree_exists() {
+ local AGENT_ID=$1
+ local WORKTREE_DIR=$(find worktrees -type d -name "agent-${AGENT_ID}*" 2>/dev/null | head -1)
+
+ [ -n "$WORKTREE_DIR" ] && [ -d "$WORKTREE_DIR" ]
+}
+
+# Main CLI (only run if executed directly, not sourced)
+if [ "${BASH_SOURCE[0]:-}" = "${0:-}" ]; then
+ case "${1:-help}" in
+ create)
+ create_agent_worktree "$2" "${3:-}" "${4:-}"
+ ;;
+ cleanup)
+ cleanup_agent_worktree "$2" "${3:-false}"
+ ;;
+ list)
+ list_agent_worktrees
+ ;;
+ merge)
+ merge_agent_work "$2"
+ ;;
+ exists)
+ worktree_exists "$2"
+ ;;
+ *)
+ echo "Usage: git-worktree-utils.sh {create|cleanup|list|merge|exists} [args]"
+ exit 1
+ ;;
+ esac
+fi
diff --git a/claude-code-4.5/utils/orchestrator-agent.sh b/claude-code-4.5/utils/orchestrator-agent.sh
new file mode 100755
index 0000000..4724dac
--- /dev/null
+++ b/claude-code-4.5/utils/orchestrator-agent.sh
@@ -0,0 +1,120 @@
+#!/bin/bash
+
+# Agent Lifecycle Management Utility
+# Handles agent spawning, status detection, and termination
+
+set -euo pipefail
+
+# Source the spawn-agent logic
+SPAWN_AGENT_CMD="${HOME}/.claude/commands/spawn-agent.md"
+
+# detect_agent_status
+# Detects agent status from tmux output
+detect_agent_status() {
+ local tmux_session="$1"
+
+ if ! tmux has-session -t "$tmux_session" 2>/dev/null; then
+ echo "killed"
+ return 0
+ fi
+
+ local output=$(tmux capture-pane -t "$tmux_session" -p -S -100 2>/dev/null || echo "")
+
+ # Check for completion indicators
+ if echo "$output" | grep -qiE "complete|done|finished|✅.*complete"; then
+ if echo "$output" | grep -qE "git.*commit|Commit.*created"; then
+ echo "complete"
+ return 0
+ fi
+ fi
+
+ # Check for failure indicators
+ if echo "$output" | grep -qiE "error|failed|❌|fatal"; then
+ echo "failed"
+ return 0
+ fi
+
+ # Check for idle (no recent activity)
+ local last_line=$(echo "$output" | tail -1)
+ if echo "$last_line" | grep -qE "^>|^│|^─|Style:|bypass permissions"; then
+ echo "idle"
+ return 0
+ fi
+
+ # Active by default
+ echo "active"
+}
+
+# check_idle_timeout
+# Checks if agent has been idle too long
+check_idle_timeout() {
+ local session_id="$1"
+ local agent_id="$2"
+ local timeout_minutes="$3"
+
+ # Get agent's last_updated timestamp
+ local last_updated=$(~/.claude/utils/orchestrator-state.sh get-agent "$session_id" "$agent_id" | jq -r '.last_updated // empty')
+
+ if [ -z "$last_updated" ]; then
+ echo "false"
+ return 0
+ fi
+
+ local now=$(date +%s)
+ local last=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${last_updated:0:19}" +%s 2>/dev/null || echo "$now")
+ local diff=$(( (now - last) / 60 ))
+
+ if [ "$diff" -gt "$timeout_minutes" ]; then
+ echo "true"
+ else
+ echo "false"
+ fi
+}
+
+# kill_agent
+# Kills an agent tmux session
+kill_agent() {
+ local tmux_session="$1"
+
+ if tmux has-session -t "$tmux_session" 2>/dev/null; then
+ tmux kill-session -t "$tmux_session"
+ echo "Killed agent session: $tmux_session"
+ fi
+}
+
+# extract_cost_from_tmux
+# Extracts cost from Claude status bar in tmux
+extract_cost_from_tmux() {
+ local tmux_session="$1"
+
+ local output=$(tmux capture-pane -t "$tmux_session" -p -S -50 2>/dev/null || echo "")
+
+ # Look for "Cost: $X.XX" pattern
+ local cost=$(echo "$output" | grep -oE 'Cost:\s*\$[0-9]+\.[0-9]{2}' | tail -1 | grep -oE '[0-9]+\.[0-9]{2}')
+
+ echo "${cost:-0.00}"
+}
+
+case "${1:-}" in
+ detect-status)
+ detect_agent_status "$2"
+ ;;
+ check-idle)
+ check_idle_timeout "$2" "$3" "$4"
+ ;;
+ kill)
+ kill_agent "$2"
+ ;;
+ extract-cost)
+ extract_cost_from_tmux "$2"
+ ;;
+ *)
+ echo "Usage: orchestrator-agent.sh [args...]"
+ echo "Commands:"
+ echo " detect-status "
+ echo " check-idle "
+ echo " kill "
+ echo " extract-cost "
+ exit 1
+ ;;
+esac
diff --git a/claude-code-4.5/utils/orchestrator-dag.sh b/claude-code-4.5/utils/orchestrator-dag.sh
new file mode 100755
index 0000000..b0b9c03
--- /dev/null
+++ b/claude-code-4.5/utils/orchestrator-dag.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+
+# DAG (Directed Acyclic Graph) Utility
+# Handles dependency resolution and wave calculation
+
+set -euo pipefail
+
+STATE_DIR="${HOME}/.claude/orchestration/state"
+
+# topological_sort
+# Returns nodes in topological order (waves)
+topological_sort() {
+ local dag_file="$1"
+
+ # Extract nodes and edges
+ local nodes=$(jq -r '.nodes | keys[]' "$dag_file")
+ local edges=$(jq -r '.edges' "$dag_file")
+
+ # Calculate in-degree for each node
+ declare -A indegree
+ for node in $nodes; do
+ local deps=$(jq -r --arg n "$node" '.edges[] | select(.to == $n) | .from' "$dag_file" | wc -l)
+ indegree[$node]=$deps
+ done
+
+ # Topological sort using Kahn's algorithm
+ local wave=1
+ local result=""
+
+ while [ ${#indegree[@]} -gt 0 ]; do
+ local wave_nodes=""
+
+ # Find all nodes with indegree 0
+ for node in "${!indegree[@]}"; do
+ if [ "${indegree[$node]}" -eq 0 ]; then
+ wave_nodes="$wave_nodes $node"
+ fi
+ done
+
+ if [ -z "$wave_nodes" ]; then
+ echo "Error: Cycle detected in DAG" >&2
+ return 1
+ fi
+
+ # Output wave
+ echo "$wave:$wave_nodes"
+
+ # Remove processed nodes and update indegrees
+ for node in $wave_nodes; do
+ unset indegree[$node]
+
+ # Decrease indegree for dependent nodes
+ local dependents=$(jq -r --arg n "$node" '.edges[] | select(.from == $n) | .to' "$dag_file")
+ for dep in $dependents; do
+ if [ -n "${indegree[$dep]:-}" ]; then
+ indegree[$dep]=$((indegree[$dep] - 1))
+ fi
+ done
+ done
+
+ ((wave++))
+ done
+}
+
+# check_dependencies
+# Checks if all dependencies for a node are satisfied
+check_dependencies() {
+ local dag_file="$1"
+ local node_id="$2"
+
+ local deps=$(jq -r --arg n "$node_id" '.edges[] | select(.to == $n) | .from' "$dag_file")
+
+ if [ -z "$deps" ]; then
+ echo "true"
+ return 0
+ fi
+
+ # Check if all dependencies are complete
+ for dep in $deps; do
+ local status=$(jq -r --arg n "$dep" '.nodes[$n].status' "$dag_file")
+ if [ "$status" != "complete" ]; then
+ echo "false"
+ return 1
+ fi
+ done
+
+ echo "true"
+}
+
+# get_next_wave
+# Gets the next wave of nodes ready to execute
+get_next_wave() {
+ local dag_file="$1"
+
+ local nodes=$(jq -r '.nodes | to_entries[] | select(.value.status == "pending") | .key' "$dag_file")
+
+ local wave_nodes=""
+ for node in $nodes; do
+ if [ "$(check_dependencies "$dag_file" "$node")" = "true" ]; then
+ wave_nodes="$wave_nodes $node"
+ fi
+ done
+
+ echo "$wave_nodes" | tr -s ' '
+}
+
+case "${1:-}" in
+ topo-sort)
+ topological_sort "$2"
+ ;;
+ check-deps)
+ check_dependencies "$2" "$3"
+ ;;
+ next-wave)
+ get_next_wave "$2"
+ ;;
+ *)
+ echo "Usage: orchestrator-dag.sh [args...]"
+ echo "Commands:"
+ echo " topo-sort "
+ echo " check-deps "
+ echo " next-wave "
+ exit 1
+ ;;
+esac
diff --git a/claude-code-4.5/utils/orchestrator-state.sh b/claude-code-4.5/utils/orchestrator-state.sh
new file mode 100755
index 0000000..40b9a57
--- /dev/null
+++ b/claude-code-4.5/utils/orchestrator-state.sh
@@ -0,0 +1,431 @@
+#!/bin/bash
+
+# Orchestrator State Management Utility
+# Manages sessions.json, completed.json, and DAG state files
+
+set -euo pipefail
+
+# Paths
+STATE_DIR="${HOME}/.claude/orchestration/state"
+SESSIONS_FILE="${STATE_DIR}/sessions.json"
+COMPLETED_FILE="${STATE_DIR}/completed.json"
+CONFIG_FILE="${STATE_DIR}/config.json"
+
+# Ensure jq is available
+if ! command -v jq &> /dev/null; then
+ echo "Error: jq is required but not installed. Install with: brew install jq"
+ exit 1
+fi
+
+# ============================================================================
+# Session Management Functions
+# ============================================================================
+
+# create_session [config_json]
+# Creates a new orchestration session
+create_session() {
+ local session_id="$1"
+ local tmux_session="$2"
+ local custom_config="${3:-{}}"
+
+ # Load default config
+ local default_config=$(jq -r '.orchestrator' "$CONFIG_FILE")
+
+ # Merge custom config with defaults
+ local merged_config=$(echo "$default_config" | jq ". + $custom_config")
+
+ # Create session object
+ local session=$(cat < "$SESSIONS_FILE"
+
+ echo "$session_id"
+}
+
+# get_session
+# Retrieves a session by ID
+get_session() {
+ local session_id="$1"
+ jq -r ".active_sessions[] | select(.session_id == \"$session_id\")" "$SESSIONS_FILE"
+}
+
+# update_session
+# Updates a session with new data (merges)
+update_session() {
+ local session_id="$1"
+ local update="$2"
+
+ local updated=$(jq \
+ --arg id "$session_id" \
+ --argjson upd "$update" \
+ '(.active_sessions[] | select(.session_id == $id)) |= (. + $upd) | .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# update_session_status
+# Updates session status
+update_session_status() {
+ local session_id="$1"
+ local status="$2"
+
+ update_session "$session_id" "{\"status\": \"$status\"}"
+}
+
+# archive_session
+# Moves session from active to completed
+archive_session() {
+ local session_id="$1"
+
+ # Get session data
+ local session=$(get_session "$session_id")
+
+ if [ -z "$session" ]; then
+ echo "Error: Session $session_id not found"
+ return 1
+ fi
+
+ # Mark as complete with end time
+ local completed_session=$(echo "$session" | jq ". + {\"completed_at\": \"$(date -Iseconds)\"}")
+
+ # Add to completed sessions
+ local updated_completed=$(jq ".completed_sessions += [$completed_session] | .last_updated = \"$(date -Iseconds)\"" "$COMPLETED_FILE")
+ echo "$updated_completed" > "$COMPLETED_FILE"
+
+ # Update totals
+ local total_cost=$(echo "$completed_session" | jq -r '.total_cost_usd')
+ local updated_totals=$(jq \
+ --arg cost "$total_cost" \
+ '.total_cost_usd += ($cost | tonumber) | .total_agents_spawned += 1' \
+ "$COMPLETED_FILE")
+ echo "$updated_totals" > "$COMPLETED_FILE"
+
+ # Remove from active sessions
+ local updated_active=$(jq \
+ --arg id "$session_id" \
+ '.active_sessions = [.active_sessions[] | select(.session_id != $id)] | .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+ echo "$updated_active" > "$SESSIONS_FILE"
+
+ echo "Session $session_id archived"
+}
+
+# list_active_sessions
+# Lists all active sessions
+list_active_sessions() {
+ jq -r '.active_sessions[] | .session_id' "$SESSIONS_FILE"
+}
+
+# ============================================================================
+# Agent Management Functions
+# ============================================================================
+
+# add_agent
+# Adds an agent to a session
+add_agent() {
+ local session_id="$1"
+ local agent_id="$2"
+ local agent_config="$3"
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ --argjson cfg "$agent_config" \
+ '(.active_sessions[] | select(.session_id == $sid).agents[$aid]) = $cfg | .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# update_agent_status
+# Updates an agent's status
+update_agent_status() {
+ local session_id="$1"
+ local agent_id="$2"
+ local status="$3"
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ --arg st "$status" \
+ '(.active_sessions[] | select(.session_id == $sid).agents[$aid].status) = $st |
+ (.active_sessions[] | select(.session_id == $sid).agents[$aid].last_updated) = "'$(date -Iseconds)'" |
+ .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# update_agent_cost
+# Updates an agent's cost
+update_agent_cost() {
+ local session_id="$1"
+ local agent_id="$2"
+ local cost_usd="$3"
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ --arg cost "$cost_usd" \
+ '(.active_sessions[] | select(.session_id == $sid).agents[$aid].cost_usd) = ($cost | tonumber) |
+ .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+
+ # Update session total cost
+ update_session_total_cost "$session_id"
+}
+
+# update_session_total_cost
+# Recalculates and updates session total cost
+update_session_total_cost() {
+ local session_id="$1"
+
+ local total=$(jq -r \
+ --arg sid "$session_id" \
+ '(.active_sessions[] | select(.session_id == $sid).agents | to_entries | map(.value.cost_usd // 0) | add) // 0' \
+ "$SESSIONS_FILE")
+
+ update_session "$session_id" "{\"total_cost_usd\": $total}"
+}
+
+# get_agent
+# Gets agent data
+get_agent() {
+ local session_id="$1"
+ local agent_id="$2"
+
+ jq -r \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ '.active_sessions[] | select(.session_id == $sid).agents[$aid]' \
+ "$SESSIONS_FILE"
+}
+
+# list_agents
+# Lists all agents in a session
+list_agents() {
+ local session_id="$1"
+
+ jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).agents | keys[]' \
+ "$SESSIONS_FILE"
+}
+
+# ============================================================================
+# Wave Management Functions
+# ============================================================================
+
+# add_wave
+# Adds a wave to the session
+add_wave() {
+ local session_id="$1"
+ local wave_number="$2"
+ local agent_ids="$3" # JSON array like '["agent-1", "agent-2"]'
+
+ local wave=$(cat < "$SESSIONS_FILE"
+}
+
+# update_wave_status
+# Updates wave status
+update_wave_status() {
+ local session_id="$1"
+ local wave_number="$2"
+ local status="$3"
+
+ local timestamp_field=""
+ if [ "$status" = "active" ]; then
+ timestamp_field="started_at"
+ elif [ "$status" = "complete" ] || [ "$status" = "failed" ]; then
+ timestamp_field="completed_at"
+ fi
+
+ local jq_filter='(.active_sessions[] | select(.session_id == $sid).waves[] | select(.wave_number == ($wn | tonumber)).status) = $st'
+
+ if [ -n "$timestamp_field" ]; then
+ jq_filter="$jq_filter | (.active_sessions[] | select(.session_id == \$sid).waves[] | select(.wave_number == (\$wn | tonumber)).$timestamp_field) = \"$(date -Iseconds)\""
+ fi
+
+ jq_filter="$jq_filter | .last_updated = \"$(date -Iseconds)\""
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg wn "$wave_number" \
+ --arg st "$status" \
+ "$jq_filter" \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# get_current_wave
+# Gets the current active or next pending wave number
+get_current_wave() {
+ local session_id="$1"
+
+ # First check for active waves
+ local active_wave=$(jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).waves[] | select(.status == "active") | .wave_number' \
+ "$SESSIONS_FILE" | head -1)
+
+ if [ -n "$active_wave" ]; then
+ echo "$active_wave"
+ return
+ fi
+
+ # Otherwise get first pending wave
+ local pending_wave=$(jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).waves[] | select(.status == "pending") | .wave_number' \
+ "$SESSIONS_FILE" | head -1)
+
+ echo "${pending_wave:-0}"
+}
+
+# ============================================================================
+# Utility Functions
+# ============================================================================
+
+# check_budget_limit
+# Checks if session is within budget limits
+check_budget_limit() {
+ local session_id="$1"
+
+ local max_budget=$(jq -r '.resource_limits.max_budget_usd' "$CONFIG_FILE")
+ local warn_percent=$(jq -r '.resource_limits.warn_at_percent' "$CONFIG_FILE")
+ local stop_percent=$(jq -r '.resource_limits.hard_stop_at_percent' "$CONFIG_FILE")
+
+ local current_cost=$(jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).total_cost_usd' \
+ "$SESSIONS_FILE")
+
+ local percent=$(echo "scale=2; ($current_cost / $max_budget) * 100" | bc)
+
+ if (( $(echo "$percent >= $stop_percent" | bc -l) )); then
+ echo "STOP"
+ return 1
+ elif (( $(echo "$percent >= $warn_percent" | bc -l) )); then
+ echo "WARN"
+ return 0
+ else
+ echo "OK"
+ return 0
+ fi
+}
+
+# pretty_print_session
+# Pretty prints a session
+pretty_print_session() {
+ local session_id="$1"
+ get_session "$session_id" | jq '.'
+}
+
+# ============================================================================
+# Main CLI Interface
+# ============================================================================
+
+case "${1:-}" in
+ create)
+ create_session "$2" "$3" "${4:-{}}"
+ ;;
+ get)
+ get_session "$2"
+ ;;
+ update)
+ update_session "$2" "$3"
+ ;;
+ archive)
+ archive_session "$2"
+ ;;
+ list)
+ list_active_sessions
+ ;;
+ add-agent)
+ add_agent "$2" "$3" "$4"
+ ;;
+ update-agent-status)
+ update_agent_status "$2" "$3" "$4"
+ ;;
+ update-agent-cost)
+ update_agent_cost "$2" "$3" "$4"
+ ;;
+ get-agent)
+ get_agent "$2" "$3"
+ ;;
+ list-agents)
+ list_agents "$2"
+ ;;
+ add-wave)
+ add_wave "$2" "$3" "$4"
+ ;;
+ update-wave-status)
+ update_wave_status "$2" "$3" "$4"
+ ;;
+ get-current-wave)
+ get_current_wave "$2"
+ ;;
+ check-budget)
+ check_budget_limit "$2"
+ ;;
+ print)
+ pretty_print_session "$2"
+ ;;
+ *)
+ echo "Usage: orchestrator-state.sh [args...]"
+ echo ""
+ echo "Commands:"
+ echo " create [config_json]"
+ echo " get "
+ echo " update "
+ echo " archive "
+ echo " list"
+ echo " add-agent "
+ echo " update-agent-status "
+ echo " update-agent-cost "
+ echo " get-agent "
+ echo " list-agents "
+ echo " add-wave "
+ echo " update-wave-status "
+ echo " get-current-wave "
+ echo " check-budget "
+ echo " print "
+ exit 1
+ ;;
+esac
diff --git a/claude-code/CLAUDE.md b/claude-code/CLAUDE.md
index c30d6d1..b9ee44e 100644
--- a/claude-code/CLAUDE.md
+++ b/claude-code/CLAUDE.md
@@ -847,11 +847,12 @@ const applyDiscount = (price: number, discountRate: number): number => {
# Background Process Management
-CRITICAL: When starting any long-running server process (web servers, development servers, APIs, etc.), you MUST:
+CRITICAL: When starting any long-running server process (web servers, development servers, APIs, etc.), you MUST use tmux for persistence and management:
-1. **Always Run in Background**
+1. **Always Run in tmux Sessions**
- NEVER run servers in foreground as this will block the agent process indefinitely
- - Use background execution (`&` or `nohup`) or container-use background mode
+ - ALWAYS use tmux for background execution (provides persistence across disconnects)
+ - Fallback to container-use background mode if tmux unavailable
- Examples of foreground-blocking commands:
- `npm run dev` or `npm start`
- `python app.py` or `flask run`
@@ -863,96 +864,164 @@ CRITICAL: When starting any long-running server process (web servers, developmen
- ALWAYS use random/dynamic ports to avoid conflicts between parallel sessions
- Generate random port: `PORT=$(shuf -i 3000-9999 -n 1)`
- Pass port via environment variable or command line argument
- - Document the assigned port in logs for reference
+ - Document the assigned port in session metadata
-3. **Mandatory Log Redirection**
- - Redirect all output to log files: `command > app.log 2>&1 &`
- - Use descriptive log names: `server.log`, `api.log`, `dev-server.log`
- - Include port in log name when possible: `server-${PORT}.log`
- - Capture both stdout and stderr for complete debugging information
+3. **tmux Session Naming Convention**
+ - Dev environments: `dev-{project}-{timestamp}`
+ - Spawned agents: `agent-{timestamp}`
+ - Monitoring: `monitor-{purpose}`
+ - Examples: `dev-myapp-1705161234`, `agent-1705161234`
-4. **Container-use Background Mode**
- - When using container-use, ALWAYS set `background: true` for server commands
- - Use `ports` parameter to expose the randomly assigned port
- - Example: `mcp__container-use__environment_run_cmd` with `background: true, ports: [PORT]`
+4. **Session Metadata**
+ - Save session info to `.tmux-dev-session.json` (per project)
+ - Include: session name, ports, services, created timestamp
+ - Use metadata for session discovery and conflict detection
-5. **Log Monitoring**
- - After starting background process, immediately check logs with `tail -f logfile.log`
- - Use `cat logfile.log` to view full log contents
- - Monitor startup messages to ensure server started successfully
- - Look for port assignment confirmation in logs
+5. **Log Capture**
+ - Use `| tee logfile.log` to capture output to both tmux and file
+ - Use descriptive log names: `server.log`, `api.log`, `dev-server.log`
+ - Include port in log name when possible: `server-${PORT}.log`
+ - Logs visible in tmux pane AND saved to disk
6. **Safe Process Management**
- - NEVER kill by process name (`pkill node`, `pkill vite`, `pkill uv`) - this affects other parallel sessions
+ - NEVER kill by process name (`pkill node`, `pkill vite`, `pkill uv`) - affects other sessions
- ALWAYS kill by port to target specific server: `lsof -ti:${PORT} | xargs kill -9`
- - Alternative port-based killing: `fuser -k ${PORT}/tcp`
- - Check what's running on port before killing: `lsof -i :${PORT}`
- - Clean up port-specific processes before starting new servers on same port
+ - Alternative: Kill entire tmux session: `tmux kill-session -t {session-name}`
+ - Check what's running on port: `lsof -i :${PORT}`
**Examples:**
```bash
-# ❌ WRONG - Will block forever and use default port
+# ❌ WRONG - Will block forever
npm run dev
# ❌ WRONG - Killing by process name affects other sessions
pkill node
-# ✅ CORRECT - Complete workflow with random port
+# ❌ DEPRECATED - Using & background jobs (no persistence)
PORT=$(shuf -i 3000-9999 -n 1)
-echo "Starting server on port $PORT"
PORT=$PORT npm run dev > dev-server-${PORT}.log 2>&1 &
-tail -f dev-server-${PORT}.log
+
+# ✅ CORRECT - Complete tmux workflow with random port
+PORT=$(shuf -i 3000-9999 -n 1)
+SESSION="dev-$(basename $(pwd))-$(date +%s)"
+
+# Create tmux session
+tmux new-session -d -s "$SESSION" -n dev-server
+
+# Start server in tmux with log capture
+tmux send-keys -t "$SESSION:dev-server" "PORT=$PORT npm run dev | tee dev-server-${PORT}.log" C-m
+
+# Save metadata
+cat > .tmux-dev-session.json </dev/null && echo "Session running"
-# ✅ CORRECT - Container-use with random port
+# ✅ CORRECT - Attach to monitor logs
+tmux attach -t "$SESSION"
+
+# ✅ CORRECT - Flask/Python in tmux
+PORT=$(shuf -i 5000-5999 -n 1)
+SESSION="dev-flask-$(date +%s)"
+tmux new-session -d -s "$SESSION" -n server
+tmux send-keys -t "$SESSION:server" "FLASK_RUN_PORT=$PORT flask run | tee flask-${PORT}.log" C-m
+
+# ✅ CORRECT - Next.js in tmux
+PORT=$(shuf -i 3000-3999 -n 1)
+SESSION="dev-nextjs-$(date +%s)"
+tmux new-session -d -s "$SESSION" -n server
+tmux send-keys -t "$SESSION:server" "PORT=$PORT npm run dev | tee nextjs-${PORT}.log" C-m
+```
+
+**Fallback: Container-use Background Mode** (when tmux unavailable):
+```bash
+# Only use if tmux is not available
mcp__container-use__environment_run_cmd with:
command: "PORT=${PORT} npm run dev"
background: true
ports: [PORT]
-
-# ✅ CORRECT - Flask/Python example
-PORT=$(shuf -i 3000-9999 -n 1)
-FLASK_RUN_PORT=$PORT python app.py > flask-${PORT}.log 2>&1 &
-
-# ✅ CORRECT - Next.js example
-PORT=$(shuf -i 3000-9999 -n 1)
-PORT=$PORT npm run dev > nextjs-${PORT}.log 2>&1 &
```
-**Playwright Testing Background Execution:**
+**Playwright Testing in tmux:**
-- **ALWAYS run Playwright tests in background** to prevent agent blocking
-- **NEVER open test report servers** - they will block agent execution indefinitely
-- Use `--reporter=json` and `--reporter=line` for programmatic result parsing
-- Redirect all output to log files for later analysis
+- **Run Playwright tests in tmux** for persistence and log monitoring
+- **NEVER open test report servers** - they block agent execution
+- Use `--reporter=json` and `--reporter=line` for programmatic parsing
- Examples:
```bash
-# ✅ CORRECT - Background Playwright execution
-npx playwright test --reporter=json > playwright-results.log 2>&1 &
+# ✅ CORRECT - Playwright in tmux session
+SESSION="test-playwright-$(date +%s)"
+tmux new-session -d -s "$SESSION" -n tests
+tmux send-keys -t "$SESSION:tests" "npx playwright test --reporter=json | tee playwright-results.log" C-m
-# ✅ CORRECT - Custom config with background execution
-npx playwright test --config=custom.config.js --reporter=line > test-output.log 2>&1 &
+# Monitor progress
+tmux attach -t "$SESSION"
+
+# ❌ DEPRECATED - Background job (no persistence)
+npx playwright test --reporter=json > playwright-results.log 2>&1 &
# ❌ WRONG - Will block agent indefinitely
npx playwright test --reporter=html
npx playwright show-report
# ✅ CORRECT - Parse results programmatically
-cat playwright-results.json | jq '.stats'
-tail -20 test-output.log
+cat playwright-results.log | jq '.stats'
```
+**Using Generic /start-* Commands:**
+
+For common development scenarios, use the generic commands:
+
+```bash
+# Start local web development (auto-detects framework)
+/start-local development # Uses .env.development
+/start-local staging # Uses .env.staging
+/start-local production # Uses .env.production
+
+# Start iOS development (auto-detects project type)
+/start-ios Debug # Uses .env.development
+/start-ios Staging # Uses .env.staging
+/start-ios Release # Uses .env.production
+
+# Start Android development (auto-detects project type)
+/start-android debug # Uses .env.development
+/start-android staging # Uses .env.staging
+/start-android release # Uses .env.production
+```
-RATIONALE: Background execution with random ports prevents agent process deadlock while enabling parallel sessions to coexist without interference. Port-based process management ensures safe cleanup without affecting other concurrent development sessions. This maintains full visibility into server status through logs while ensuring continuous agent operation.
+These commands automatically:
+- Create organized tmux sessions
+- Assign random ports
+- Start all required services
+- Save session metadata
+- Setup log monitoring
+
+**Session Persistence Benefits:**
+- Survives SSH disconnects
+- Survives terminal restarts
+- Easy reattachment: `tmux attach -t {session-name}`
+- Live log monitoring in split panes
+- Organized multi-window layouts
+
+RATIONALE: tmux provides persistence across disconnects, better visibility through split panes, and session organization. Random ports prevent conflicts between parallel sessions. Port-based or session-based process management ensures safe cleanup. Generic /start-* commands provide consistent, framework-agnostic development environments.
# GitHub Issue Management
diff --git a/claude-code/commands/attach-agent-worktree.md b/claude-code/commands/attach-agent-worktree.md
new file mode 100644
index 0000000..a141096
--- /dev/null
+++ b/claude-code/commands/attach-agent-worktree.md
@@ -0,0 +1,46 @@
+# /attach-agent-worktree - Attach to Agent Session
+
+Changes to agent worktree directory and attaches to its tmux session.
+
+## Usage
+
+```bash
+/attach-agent-worktree {timestamp}
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+AGENT_ID="$1"
+
+if [ -z "$AGENT_ID" ]; then
+ echo "❌ Agent ID required"
+ echo "Usage: /attach-agent-worktree {timestamp}"
+ exit 1
+fi
+
+# Find worktree directory
+WORKTREE_DIR=$(find worktrees -type d -name "agent-${AGENT_ID}*" 2>/dev/null | head -1)
+
+if [ -z "$WORKTREE_DIR" ] || [ ! -d "$WORKTREE_DIR" ]; then
+ echo "❌ Worktree not found for agent: $AGENT_ID"
+ exit 1
+fi
+
+SESSION="agent-${AGENT_ID}"
+
+# Check if tmux session exists
+if ! tmux has-session -t "$SESSION" 2>/dev/null; then
+ echo "❌ Tmux session not found: $SESSION"
+ exit 1
+fi
+
+echo "📂 Worktree: $WORKTREE_DIR"
+echo "🔗 Attaching to session: $SESSION"
+echo ""
+
+# Attach to session
+tmux attach -t "$SESSION"
+```
diff --git a/claude-code/commands/cleanup-agent-worktree.md b/claude-code/commands/cleanup-agent-worktree.md
new file mode 100644
index 0000000..12f7ebb
--- /dev/null
+++ b/claude-code/commands/cleanup-agent-worktree.md
@@ -0,0 +1,36 @@
+# /cleanup-agent-worktree - Remove Agent Worktree
+
+Removes a specific agent worktree and its branch.
+
+## Usage
+
+```bash
+/cleanup-agent-worktree {timestamp}
+/cleanup-agent-worktree {timestamp} --force
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+AGENT_ID="$1"
+FORCE="$2"
+
+if [ -z "$AGENT_ID" ]; then
+ echo "❌ Agent ID required"
+ echo "Usage: /cleanup-agent-worktree {timestamp} [--force]"
+ exit 1
+fi
+
+# Source utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/../utils/git-worktree-utils.sh"
+
+# Cleanup worktree
+if [ "$FORCE" = "--force" ]; then
+ cleanup_agent_worktree "$AGENT_ID" true
+else
+ cleanup_agent_worktree "$AGENT_ID" false
+fi
+```
diff --git a/claude-code/commands/handover.md b/claude-code/commands/handover.md
index e57f076..36a9e37 100644
--- a/claude-code/commands/handover.md
+++ b/claude-code/commands/handover.md
@@ -1,59 +1,187 @@
-# Handover Command
+# /handover - Generate Session Handover Document
-Use this command to generate a session handover document when transferring work to another team member or continuing work in a new session.
+Generate a handover document for transferring work to another developer or spawning an async agent.
## Usage
-```
-/handover [optional-notes]
+```bash
+/handover # Standard handover
+/handover "notes about current work" # With notes
+/handover --agent-spawn "task desc" # For spawning agent
```
-## Description
+## Modes
-This command generates a comprehensive handover document that includes:
+### Standard Handover (default)
-- Current session health status
+For transferring work to another human or resuming later:
+- Current session health
- Task progress and todos
-- Technical context and working files
-- Instructions for resuming work
-- Any blockers or important notes
+- Technical context
+- Resumption instructions
-## Example
+### Agent Spawn Mode (`--agent-spawn`)
+For passing context to spawned agents:
+- Focused on task context
+- Technical stack details
+- Success criteria
+- Files to modify
+
+## Implementation
+
+### Detect Mode
+
+```bash
+MODE="standard"
+AGENT_TASK=""
+NOTES="${1:-}"
+
+if [[ "$1" == "--agent-spawn" ]]; then
+ MODE="agent"
+ AGENT_TASK="${2:-}"
+ shift 2
+fi
```
-/handover Working on authentication refactor, need to complete OAuth integration
+
+### Generate Timestamp
+
+```bash
+TIMESTAMP=$(date +"%Y-%m-%d-%H-%M-%S")
+DISPLAY_TIME=$(date +"%Y-%m-%d %H:%M:%S")
+FILENAME="handover-${TIMESTAMP}.md"
+PRIMARY_LOCATION="${TOOL_DIR}/session/${FILENAME}"
+BACKUP_LOCATION="./${FILENAME}"
+
+mkdir -p "${TOOL_DIR}/session"
```
-## Output Location
+### Standard Handover Content
+
+```markdown
+# Handover Document
+
+**Generated**: ${DISPLAY_TIME}
+**Session**: $(tmux display-message -p '#S' 2>/dev/null || echo 'unknown')
+
+## Current Work
+
+[Describe what you're working on]
+
+## Task Progress
+
+[List todos and completion status]
+
+## Technical Context
+
+**Current Branch**: $(git branch --show-current)
+**Last Commit**: $(git log -1 --oneline)
+**Modified Files**:
+$(git status --short)
+
+## Resumption Instructions
+
+1. Review changes: git diff
+2. Continue work on [specific task]
+3. Test with: [test command]
+
+## Notes
+
+${NOTES}
+```
+
+### Agent Spawn Handover Content
+
+```markdown
+# Agent Handover - ${AGENT_TASK}
+
+**Generated**: ${DISPLAY_TIME}
+**Parent Session**: $(tmux display-message -p '#S' 2>/dev/null || echo 'unknown')
+**Agent Task**: ${AGENT_TASK}
+
+## Context Summary
+
+**Current Work**: [What's in progress]
+**Current Branch**: $(git branch --show-current)
+**Last Commit**: $(git log -1 --oneline)
+
+## Task Details
+
+**Agent Mission**: ${AGENT_TASK}
+
+**Requirements**:
+- [List specific requirements]
+- [What needs to be done]
+
+**Success Criteria**:
+- [How to know when done]
+
+## Technical Context
+
+**Stack**: [Technology stack]
+**Key Files**:
+$(git status --short)
+
+**Modified Recently**:
+$(git log --name-only -5 --oneline)
-The handover document MUST be saved to:
-- **Primary Location**: `.{{TOOL_DIR}}/session/handover-{{TIMESTAMP}}.md`
-- **Backup Location**: `./handover-{{TIMESTAMP}}.md` (project root)
+## Instructions for Agent
-## File Naming Convention
+1. Review current implementation
+2. Make specified changes
+3. Add/update tests
+4. Verify all tests pass
+5. Commit with clear message
-Use this format: `handover-YYYY-MM-DD-HH-MM-SS.md`
+## References
-Example: `handover-2024-01-15-14-30-45.md`
+**Documentation**: [Links to relevant docs]
+**Related Work**: [Related PRs/issues]
+```
+
+### Save Document
-**CRITICAL**: Always obtain the timestamp programmatically:
```bash
-# Generate timestamp - NEVER type dates manually
-TIMESTAMP=$(date +"%Y-%m-%d-%H-%M-%S")
-FILENAME="handover-${TIMESTAMP}.md"
+# Generate appropriate content based on MODE
+if [ "$MODE" = "agent" ]; then
+ # Generate agent handover content
+ CONTENT="[Agent handover content from above]"
+else
+ # Generate standard handover content
+ CONTENT="[Standard handover content from above]"
+fi
+
+# Save to primary location
+echo "$CONTENT" > "$PRIMARY_LOCATION"
+
+# Save backup
+echo "$CONTENT" > "$BACKUP_LOCATION"
+
+echo "✅ Handover document generated"
+echo ""
+echo "Primary: $PRIMARY_LOCATION"
+echo "Backup: $BACKUP_LOCATION"
+echo ""
```
-## Implementation
+## Output Location
+
+**Primary**: `${TOOL_DIR}/session/handover-{timestamp}.md`
+**Backup**: `./handover-{timestamp}.md`
+
+## Integration with spawn-agent
+
+The `/spawn-agent` command automatically calls `/handover --agent-spawn` when `--with-handover` flag is used:
+
+```bash
+/spawn-agent codex "refactor auth" --with-handover
+# Internally calls: /handover --agent-spawn "refactor auth"
+# Copies handover to agent worktree as .agent-handover.md
+```
+
+## Notes
-1. **ALWAYS** get the current timestamp using `date` command:
- ```bash
- date +"%Y-%m-%d %H:%M:%S" # For document header
- date +"%Y-%m-%d-%H-%M-%S" # For filename
- ```
-2. Generate handover using `{{HOME_TOOL_DIR}}/templates/handover-template.md`
-3. Replace all `{{VARIABLE}}` placeholders with actual values
-4. Save to BOTH locations (primary and backup)
-5. Display the full file path to the user for reference
-6. Verify the date in the filename matches the date in the document header
-
-The handover document will be saved as a markdown file and can be used to seamlessly continue work in a new session.
\ No newline at end of file
+- Always uses programmatic timestamps (never manual)
+- Saves to both primary and backup locations
+- Agent mode focuses on task context, not session health
+- Standard mode includes full session state
diff --git a/claude-code/commands/list-agent-worktrees.md b/claude-code/commands/list-agent-worktrees.md
new file mode 100644
index 0000000..3534902
--- /dev/null
+++ b/claude-code/commands/list-agent-worktrees.md
@@ -0,0 +1,16 @@
+# /list-agent-worktrees - List All Agent Worktrees
+
+Shows all active agent worktrees with their paths and branches.
+
+## Usage
+
+```bash
+/list-agent-worktrees
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+git worktree list | grep "worktrees/agent-" || echo "No agent worktrees found"
+```
diff --git a/claude-code/commands/m-implement.md b/claude-code/commands/m-implement.md
new file mode 100644
index 0000000..b713e44
--- /dev/null
+++ b/claude-code/commands/m-implement.md
@@ -0,0 +1,375 @@
+---
+description: Multi-agent implementation - Execute DAG in waves with automated monitoring
+tags: [orchestration, implementation, multi-agent]
+---
+
+# Multi-Agent Implementation (`/m-implement`)
+
+You are now in **multi-agent implementation mode**. Your task is to execute a pre-planned DAG by spawning agents in waves and monitoring their progress.
+
+## Your Role
+
+Act as an **orchestrator** that manages parallel agent execution, monitors progress, and handles failures.
+
+## Prerequisites
+
+1. **DAG file must exist**: `~/.claude/orchestration/state/dag-.json`
+2. **Session must be created**: Via `/m-plan` or manually
+3. **Git worktrees setup**: Project must support git worktrees
+
+## Process
+
+### Step 1: Load DAG and Session
+
+```bash
+# Load DAG file
+DAG_FILE="~/.claude/orchestration/state/dag-${SESSION_ID}.json"
+
+# Verify DAG exists
+if [ ! -f "$DAG_FILE" ]; then
+ echo "Error: DAG file not found: $DAG_FILE"
+ exit 1
+fi
+
+# Load session
+SESSION=$(~/.claude/utils/orchestrator-state.sh get "$SESSION_ID")
+
+if [ -z "$SESSION" ]; then
+ echo "Error: Session not found: $SESSION_ID"
+ exit 1
+fi
+```
+
+### Step 2: Calculate Waves
+
+```bash
+# Get waves from DAG (already calculated in /m-plan)
+WAVES=$(jq -r '.waves[] | "\(.wave_number):\(.nodes | join(" "))"' "$DAG_FILE")
+
+# Example output:
+# 1:ws-1 ws-3
+# 2:ws-2 ws-4
+# 3:ws-5
+```
+
+### Step 3: Execute Wave-by-Wave
+
+**For each wave:**
+
+```bash
+WAVE_NUMBER=1
+
+# Get nodes in this wave
+WAVE_NODES=$(echo "$WAVES" | grep "^${WAVE_NUMBER}:" | cut -d: -f2)
+
+echo "🌊 Starting Wave $WAVE_NUMBER: $WAVE_NODES"
+
+# Update wave status
+~/.claude/utils/orchestrator-state.sh update-wave-status "$SESSION_ID" "$WAVE_NUMBER" "active"
+
+# Spawn all agents in wave (parallel)
+for node in $WAVE_NODES; do
+ spawn_agent "$SESSION_ID" "$node" &
+done
+
+# Wait for all agents in wave to complete
+wait
+
+# Check if wave completed successfully
+if wave_all_complete "$SESSION_ID" "$WAVE_NUMBER"; then
+ ~/.claude/utils/orchestrator-state.sh update-wave-status "$SESSION_ID" "$WAVE_NUMBER" "complete"
+ echo "✅ Wave $WAVE_NUMBER complete"
+else
+ echo "❌ Wave $WAVE_NUMBER failed"
+ exit 1
+fi
+```
+
+### Step 4: Spawn Agent Function
+
+**Function to spawn a single agent:**
+
+```bash
+spawn_agent() {
+ local session_id="$1"
+ local node_id="$2"
+
+ # Get node details from DAG
+ local node=$(jq -r --arg n "$node_id" '.nodes[$n]' "$DAG_FILE")
+ local task=$(echo "$node" | jq -r '.task')
+ local agent_type=$(echo "$node" | jq -r '.agent_type')
+ local workstream_id=$(echo "$node" | jq -r '.workstream_id')
+
+ # Create git worktree
+ local worktree_dir="worktrees/${workstream_id}-${node_id}"
+ local branch="feat/${workstream_id}"
+
+ git worktree add "$worktree_dir" -b "$branch" 2>/dev/null || git worktree add "$worktree_dir" "$branch"
+
+ # Create tmux session
+ local agent_id="agent-${workstream_id}-$(date +%s)"
+ tmux new-session -d -s "$agent_id" -c "$worktree_dir"
+
+ # Start Claude in tmux
+ tmux send-keys -t "$agent_id" "claude --dangerously-skip-permissions" C-m
+
+ # Wait for Claude to initialize
+ wait_for_claude_ready "$agent_id"
+
+ # Send task
+ local full_task="$task
+
+AGENT ROLE: Act as a ${agent_type}.
+
+CRITICAL REQUIREMENTS:
+- Work in worktree: $worktree_dir
+- Branch: $branch
+- When complete: Run tests, commit with clear message, report status
+
+DELIVERABLES:
+$(echo "$node" | jq -r '.deliverables[]' | sed 's/^/- /')
+
+When complete: Commit all changes and report status."
+
+ tmux send-keys -t "$agent_id" -l "$full_task"
+ tmux send-keys -t "$agent_id" C-m
+
+ # Add agent to session state
+ local agent_config=$(cat <15min, killing..."
+ ~/.claude/utils/orchestrator-agent.sh kill "$tmux_session"
+ ~/.claude/utils/orchestrator-state.sh update-agent-status "$session_id" "$agent_id" "killed"
+ fi
+ done
+
+ # Check if wave is complete
+ if wave_all_complete "$session_id" "$wave_number"; then
+ return 0
+ fi
+
+ # Check if wave failed
+ local failed_count=$(~/.claude/utils/orchestrator-state.sh list-agents "$session_id" | \
+ xargs -I {} ~/.claude/utils/orchestrator-state.sh get-agent "$session_id" {} | \
+ jq -r 'select(.status == "failed")' | wc -l)
+
+ if [ "$failed_count" -gt 0 ]; then
+ echo "❌ Wave $wave_number failed ($failed_count agents failed)"
+ return 1
+ fi
+
+ # Sleep before next check
+ sleep 30
+ done
+}
+```
+
+### Step 7: Handle Completion
+
+**When all waves complete:**
+
+```bash
+# Archive session
+~/.claude/utils/orchestrator-state.sh archive "$SESSION_ID"
+
+# Print summary
+echo "🎉 All waves complete!"
+echo ""
+echo "Summary:"
+echo " Total Cost: \$$(jq -r '.total_cost_usd' sessions.json)"
+echo " Total Agents: $(jq -r '.agents | length' sessions.json)"
+echo " Duration: "
+echo ""
+echo "Next steps:"
+echo " 1. Review agent outputs in worktrees"
+echo " 2. Merge worktrees to main branch"
+echo " 3. Run integration tests"
+```
+
+## Output Format
+
+**During execution, display:**
+
+```
+🚀 Multi-Agent Implementation:
+
+📊 Plan Summary:
+ - Total Workstreams: 7
+ - Total Waves: 4
+ - Max Concurrent: 4
+
+🌊 Wave 1 (2 agents)
+ ✅ agent-ws1-xxx (complete) - Cost: $1.86
+ ✅ agent-ws3-xxx (complete) - Cost: $0.79
+ Duration: 8m 23s
+
+🌊 Wave 2 (2 agents)
+ 🔄 agent-ws2-xxx (active) - Cost: $0.45
+ 🔄 agent-ws4-xxx (active) - Cost: $0.38
+ Elapsed: 3m 12s
+
+🌊 Wave 3 (1 agent)
+ ⏸️ agent-ws5-xxx (pending)
+
+🌊 Wave 4 (2 agents)
+ ⏸️ agent-ws6-xxx (pending)
+ ⏸️ agent-ws7-xxx (pending)
+
+💰 Total Cost: $3.48 / $50.00 (7%)
+⏱️ Total Time: 11m 35s
+
+Press Ctrl+C to pause monitoring (agents continue in background)
+```
+
+## Important Notes
+
+- **Non-blocking**: Agents run in background tmux sessions
+- **Resumable**: Can exit and resume with `/m-monitor `
+- **Auto-recovery**: Idle agents are killed automatically
+- **Budget limits**: Stops if budget exceeded
+- **Parallel execution**: Multiple agents per wave (up to max_concurrent)
+
+## Error Handling
+
+**If agent fails:**
+1. Mark agent as "failed"
+2. Continue other agents in wave
+3. Do not proceed to next wave
+4. Present failure summary to user
+5. Allow manual retry or skip
+
+**If timeout:**
+1. Check if agent is actually running (may be false positive)
+2. If truly stuck, kill and mark as failed
+3. Offer retry option
+
+## Resume Support
+
+**To resume a paused/stopped session:**
+
+```bash
+/m-implement --resume
+```
+
+**Resume logic:**
+1. Load existing session state
+2. Determine current wave
+3. Check which agents are still running
+4. Continue from where it left off
+
+## CLI Options (Future)
+
+```bash
+/m-implement [options]
+
+Options:
+ --resume Resume from last checkpoint
+ --from-wave N Start from specific wave number
+ --dry-run Show what would be executed
+ --max-concurrent N Override max concurrent agents
+ --no-monitoring Spawn agents and exit (no monitoring loop)
+```
+
+## Integration with `/spawn-agent`
+
+This command reuses logic from `~/.claude/commands/spawn-agent.md`:
+- Git worktree creation
+- Claude initialization detection
+- Task sending via tmux
+
+## Exit Conditions
+
+**Success:**
+- All waves complete
+- All agents have status "complete"
+- No failures
+
+**Failure:**
+- Any agent has status "failed"
+- Budget limit exceeded
+- User manually aborts
+
+**Pause:**
+- User presses Ctrl+C
+- Session state saved
+- Agents continue in background
+- Resume with `/m-monitor `
+
+---
+
+**End of `/m-implement` command**
diff --git a/claude-code/commands/m-monitor.md b/claude-code/commands/m-monitor.md
new file mode 100644
index 0000000..b1ecee6
--- /dev/null
+++ b/claude-code/commands/m-monitor.md
@@ -0,0 +1,118 @@
+---
+description: Multi-agent monitoring - Real-time dashboard for orchestration sessions
+tags: [orchestration, monitoring, multi-agent]
+---
+
+# Multi-Agent Monitoring (`/m-monitor`)
+
+You are now in **multi-agent monitoring mode**. Display a real-time dashboard of the orchestration session status.
+
+## Your Role
+
+Act as a **monitoring dashboard** that displays live status of all agents, waves, costs, and progress.
+
+## Usage
+
+```bash
+/m-monitor
+```
+
+## Display Format
+
+```
+🚀 Multi-Agent Session: orch-1763400000
+
+📊 Plan Summary:
+ - Task: Implement BigCommerce migration
+ - Created: 2025-11-17 10:00:00
+ - Total Workstreams: 7
+ - Total Waves: 4
+ - Max Concurrent: 4
+
+🌊 Wave 1: Complete ✅ (Duration: 8m 23s)
+ ✅ agent-ws1-1763338466 (WS-1: Service Layer)
+ Status: complete | Cost: $1.86 | Branch: feat/ws-1
+ Worktree: worktrees/ws-1-service-layer
+ Last Update: 2025-11-17 10:08:23
+
+ ✅ agent-ws3-1763338483 (WS-3: Database Schema)
+ Status: complete | Cost: $0.79 | Branch: feat/ws-3
+ Worktree: worktrees/ws-3-database-schema
+ Last Update: 2025-11-17 10:08:15
+
+🌊 Wave 2: Active 🔄 (Elapsed: 3m 12s)
+ 🔄 agent-ws2-1763341887 (WS-2: Edge Functions)
+ Status: active | Cost: $0.45 | Branch: feat/ws-2
+ Worktree: worktrees/ws-2-edge-functions
+ Last Update: 2025-11-17 10:11:35
+ Attach: tmux attach -t agent-ws2-1763341887
+
+ 🔄 agent-ws4-1763341892 (WS-4: Frontend UI)
+ Status: active | Cost: $0.38 | Branch: feat/ws-4
+ Worktree: worktrees/ws-4-frontend-ui
+ Last Update: 2025-11-17 10:11:42
+ Attach: tmux attach -t agent-ws4-1763341892
+
+🌊 Wave 3: Pending ⏸️
+ ⏸️ agent-ws5-pending (WS-5: Checkout Flow)
+
+🌊 Wave 4: Pending ⏸️
+ ⏸️ agent-ws6-pending (WS-6: E2E Tests)
+ ⏸️ agent-ws7-pending (WS-7: Documentation)
+
+💰 Budget Status:
+ - Current Cost: $3.48
+ - Budget Limit: $50.00
+ - Usage: 7% 🟢
+
+⏱️ Timeline:
+ - Total Elapsed: 11m 35s
+ - Estimated Remaining: ~5h 30m
+
+📋 Commands:
+ - Refresh: /m-monitor
+ - Attach to agent: tmux attach -t
+ - View agent output: tmux capture-pane -t -p
+ - Kill idle agent: ~/.claude/utils/orchestrator-agent.sh kill
+ - Pause session: Ctrl+C (agents continue in background)
+ - Resume session: /m-implement --resume
+
+Status Legend:
+ ✅ complete 🔄 active ⏸️ pending ⚠️ idle ❌ failed 💀 killed
+```
+
+## Implementation (Phase 2)
+
+**This is a stub command for Phase 1.** Full implementation in Phase 2 will include:
+
+1. **Live monitoring loop** - Refresh every 30s
+2. **Interactive controls** - Pause, resume, kill agents
+3. **Cost tracking** - Real-time budget updates
+4. **Idle detection** - Highlight idle agents
+5. **Failure alerts** - Notify on failures
+6. **Performance metrics** - Agent completion times
+
+## Current Workaround
+
+**Until Phase 2 is complete, use these manual commands:**
+
+```bash
+# View session status
+~/.claude/utils/orchestrator-state.sh print
+
+# List all agents
+~/.claude/utils/orchestrator-state.sh list-agents
+
+# Check specific agent
+~/.claude/utils/orchestrator-state.sh get-agent
+
+# Attach to agent tmux session
+tmux attach -t
+
+# View agent output without attaching
+tmux capture-pane -t -p | tail -50
+```
+
+---
+
+**End of `/m-monitor` command (stub)**
diff --git a/claude-code/commands/m-plan.md b/claude-code/commands/m-plan.md
new file mode 100644
index 0000000..39607c7
--- /dev/null
+++ b/claude-code/commands/m-plan.md
@@ -0,0 +1,261 @@
+---
+description: Multi-agent planning - Decompose complex tasks into parallel workstreams with dependency DAG
+tags: [orchestration, planning, multi-agent]
+---
+
+# Multi-Agent Planning (`/m-plan`)
+
+You are now in **multi-agent planning mode**. Your task is to decompose a complex task into parallel workstreams with a dependency graph (DAG).
+
+## Your Role
+
+Act as a **solution-architect** specialized in task decomposition and dependency analysis.
+
+## Process
+
+### 1. Understand the Task
+
+**Ask clarifying questions if needed:**
+- What is the overall goal?
+- Are there any constraints (time, budget, resources)?
+- Are there existing dependencies or requirements?
+- What is the desired merge strategy?
+
+### 2. Decompose into Workstreams
+
+**Break down the task into independent workstreams:**
+- Each workstream should be a cohesive unit of work
+- Workstreams should be as independent as possible
+- Identify clear deliverables for each workstream
+- Assign appropriate agent types (backend-developer, frontend-developer, etc.)
+
+**Workstream Guidelines:**
+- **Size**: Each workstream should take 1-3 hours of agent time
+- **Independence**: Minimize dependencies between workstreams
+- **Clarity**: Clear, specific deliverables
+- **Agent Type**: Match to specialized agent capabilities
+
+### 3. Identify Dependencies
+
+**For each workstream, determine:**
+- What other workstreams must complete first?
+- What outputs does it depend on?
+- What outputs does it produce for others?
+
+**Dependency Types:**
+- **Blocking**: Must complete before dependent can start
+- **Data**: Provides data/files needed by dependent
+- **Interface**: Provides API/interface contract
+
+### 4. Create DAG Structure
+
+**Generate a JSON DAG file:**
+```json
+{
+ "session_id": "orch-",
+ "created_at": "",
+ "task_description": "",
+ "nodes": {
+ "ws-1-": {
+ "task": "",
+ "agent_type": "backend-developer",
+ "workstream_id": "ws-1",
+ "dependencies": [],
+ "status": "pending",
+ "deliverables": [
+ "src/services/FooService.ts",
+ "tests for FooService"
+ ]
+ },
+ "ws-2-": {
+ "task": "",
+ "agent_type": "frontend-developer",
+ "workstream_id": "ws-2",
+ "dependencies": ["ws-1"],
+ "status": "pending",
+ "deliverables": [
+ "src/components/FooComponent.tsx"
+ ]
+ }
+ },
+ "edges": [
+ {"from": "ws-1", "to": "ws-2", "type": "blocking"}
+ ]
+}
+```
+
+### 5. Calculate Waves
+
+Use the topological sort utility to calculate execution waves:
+
+```bash
+~/.claude/utils/orchestrator-dag.sh topo-sort
+```
+
+**Add wave information to DAG:**
+```json
+{
+ "waves": [
+ {
+ "wave_number": 1,
+ "nodes": ["ws-1", "ws-3"],
+ "status": "pending",
+ "estimated_parallel_time_hours": 2
+ },
+ {
+ "wave_number": 2,
+ "nodes": ["ws-2", "ws-4"],
+ "status": "pending",
+ "estimated_parallel_time_hours": 1.5
+ }
+ ]
+}
+```
+
+### 6. Estimate Costs and Timeline
+
+**For each workstream:**
+- Estimate agent time (hours)
+- Estimate cost based on historical data (~$1-2 per hour)
+- Calculate total cost and timeline
+
+**Wave-based timeline:**
+- Wave 1: 2 hours (parallel)
+- Wave 2: 1.5 hours (parallel)
+- Total: 3.5 hours (not 7 hours due to parallelism)
+
+### 7. Save DAG File
+
+**Save to:**
+```
+~/.claude/orchestration/state/dag-.json
+```
+
+**Create orchestration session:**
+```bash
+SESSION_ID=$(~/.claude/utils/orchestrator-state.sh create \
+ "orch-$(date +%s)" \
+ "orch-$(date +%s)-monitor" \
+ '{}')
+
+echo "Created session: $SESSION_ID"
+```
+
+## Output Format
+
+**Present to user:**
+
+```markdown
+# Multi-Agent Plan:
+
+## Summary
+- **Total Workstreams**: X
+- **Total Waves**: Y
+- **Estimated Timeline**: Z hours (parallel)
+- **Estimated Cost**: $A - $B
+- **Max Concurrent Agents**: 4
+
+## Workstreams
+
+### Wave 1 (No dependencies)
+- **WS-1: ** (backend-developer) -
+ - Deliverables: ...
+ - Estimated: 2h, $2
+
+- **WS-3: ** (migration) -
+ - Deliverables: ...
+ - Estimated: 1.5h, $1.50
+
+### Wave 2 (Depends on Wave 1)
+- **WS-2: ** (backend-developer) -
+ - Dependencies: WS-3 (needs database schema)
+ - Deliverables: ...
+ - Estimated: 1.5h, $1.50
+
+### Wave 3 (Depends on Wave 2)
+- **WS-4: ** (frontend-developer) -
+ - Dependencies: WS-1 (needs service interface)
+ - Deliverables: ...
+ - Estimated: 2h, $2
+
+## Dependency Graph
+```
+ WS-1
+ │
+ ├─→ WS-2
+ │
+ WS-3
+ │
+ └─→ WS-4
+```
+
+## Timeline
+- Wave 1: 2h (WS-1, WS-3 in parallel)
+- Wave 2: 1.5h (WS-2 waits for WS-3)
+- Wave 3: 2h (WS-4 waits for WS-1)
+- **Total: 5.5 hours**
+
+## Total Cost Estimate
+- **Low**: $5.00 (efficient execution)
+- **High**: $8.00 (with retries)
+
+## DAG File
+Saved to: `~/.claude/orchestration/state/dag-.json`
+
+## Next Steps
+To execute this plan:
+```bash
+/m-implement
+```
+
+To monitor progress:
+```bash
+/m-monitor
+```
+```
+
+## Important Notes
+
+- **Keep workstreams focused**: Don't create too many tiny workstreams
+- **Minimize dependencies**: More parallelism = faster completion
+- **Assign correct agent types**: Use specialized agents for best results
+- **Include all deliverables**: Be specific about what each workstream produces
+- **Estimate conservatively**: Better to over-estimate than under-estimate
+
+## Agent Types Available
+
+- `backend-developer` - Server-side code, APIs, services
+- `frontend-developer` - UI components, React, TypeScript
+- `migration` - Database schemas, Flyway migrations
+- `test-writer-fixer` - E2E tests, test suites
+- `documentation-specialist` - Docs, runbooks, guides
+- `security-agent` - Security reviews, vulnerability fixes
+- `performance-optimizer` - Performance analysis, optimization
+- `devops-automator` - CI/CD, infrastructure, deployments
+
+## Example Usage
+
+**User Request:**
+```
+/m-plan Implement authentication system with OAuth, JWT tokens, and user profile management
+```
+
+**Your Response:**
+1. Ask clarifying questions (OAuth provider? Existing DB schema?)
+2. Decompose into workstreams (auth service, OAuth integration, user profiles, frontend UI)
+3. Identify dependencies (auth service → OAuth integration → frontend)
+4. Create DAG JSON
+5. Calculate waves
+6. Estimate costs
+7. Save DAG file
+8. Present plan to user
+9. Wait for approval before proceeding
+
+**After user approves:**
+- Do NOT execute automatically
+- Instruct user to run `/m-implement `
+- Provide monitoring commands
+
+---
+
+**End of `/m-plan` command**
diff --git a/claude-code/commands/merge-agent-work.md b/claude-code/commands/merge-agent-work.md
new file mode 100644
index 0000000..f54bc4f
--- /dev/null
+++ b/claude-code/commands/merge-agent-work.md
@@ -0,0 +1,30 @@
+# /merge-agent-work - Merge Agent Branch
+
+Merges an agent's branch into the current branch.
+
+## Usage
+
+```bash
+/merge-agent-work {timestamp}
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+AGENT_ID="$1"
+
+if [ -z "$AGENT_ID" ]; then
+ echo "❌ Agent ID required"
+ echo "Usage: /merge-agent-work {timestamp}"
+ exit 1
+fi
+
+# Source utilities
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+source "$SCRIPT_DIR/../utils/git-worktree-utils.sh"
+
+# Merge agent work
+merge_agent_work "$AGENT_ID"
+```
diff --git a/claude-code/commands/spawn-agent.md b/claude-code/commands/spawn-agent.md
new file mode 100644
index 0000000..8ced240
--- /dev/null
+++ b/claude-code/commands/spawn-agent.md
@@ -0,0 +1,289 @@
+# /spawn-agent - Spawn Claude Agent in tmux Session
+
+Spawn a Claude Code agent in a separate tmux session with optional handover context.
+
+## Usage
+
+```bash
+/spawn-agent "implement user authentication"
+/spawn-agent "refactor the API layer" --with-handover
+/spawn-agent "implement feature X" --with-worktree
+/spawn-agent "review the PR" --with-worktree --with-handover
+```
+
+## Implementation
+
+```bash
+#!/bin/bash
+
+# Function: Wait for Claude Code to be ready for input
+wait_for_claude_ready() {
+ local SESSION=$1
+ local TIMEOUT=30
+ local START=$(date +%s)
+
+ echo "⏳ Waiting for Claude to initialize..."
+
+ while true; do
+ # Capture pane output (suppress errors if session not ready)
+ PANE_OUTPUT=$(tmux capture-pane -t "$SESSION" -p 2>/dev/null)
+
+ # Check for Claude prompt/splash (any of these indicates readiness)
+ if echo "$PANE_OUTPUT" | grep -qE "Claude Code|Welcome back|──────|Style:|bypass permissions"; then
+ # Verify not in error state
+ if ! echo "$PANE_OUTPUT" | grep -qiE "error|crash|failed|command not found"; then
+ echo "✅ Claude initialized successfully"
+ return 0
+ fi
+ fi
+
+ # Timeout check
+ local ELAPSED=$(($(date +%s) - START))
+ if [ $ELAPSED -gt $TIMEOUT ]; then
+ echo "❌ Timeout: Claude did not initialize within ${TIMEOUT}s"
+ echo "📋 Capturing debug output..."
+ tmux capture-pane -t "$SESSION" -p > "/tmp/spawn-agent-${SESSION}-failure.log" 2>&1
+ echo "Debug output saved to /tmp/spawn-agent-${SESSION}-failure.log"
+ return 1
+ fi
+
+ sleep 0.2
+ done
+}
+
+# Parse arguments
+TASK="$1"
+WITH_HANDOVER=false
+WITH_WORKTREE=false
+shift
+
+# Parse flags
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --with-handover)
+ WITH_HANDOVER=true
+ shift
+ ;;
+ --with-worktree)
+ WITH_WORKTREE=true
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [ -z "$TASK" ]; then
+ echo "❌ Task description required"
+ echo "Usage: /spawn-agent \"task description\" [--with-handover] [--with-worktree]"
+ exit 1
+fi
+
+# Generate session info
+TASK_ID=$(date +%s)
+SESSION="agent-${TASK_ID}"
+
+# Setup working directory (worktree or current)
+if [ "$WITH_WORKTREE" = true ]; then
+ # Detect transcrypt (informational only - works transparently with worktrees)
+ if git config --get-regexp '^transcrypt\.' >/dev/null 2>&1; then
+ echo "📦 Transcrypt detected - worktree will inherit encryption config automatically"
+ echo ""
+ fi
+
+ # Get current branch as base
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "HEAD")
+
+ # Generate task slug from task description
+ TASK_SLUG=$(echo "$TASK" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | tr -s ' ' '-' | cut -c1-40 | sed 's/-$//')
+
+ # Source worktree utilities
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ source "$SCRIPT_DIR/../utils/git-worktree-utils.sh"
+
+ # Create worktree with task slug
+ echo "🌳 Creating isolated git worktree..."
+ WORK_DIR=$(create_agent_worktree "$TASK_ID" "$CURRENT_BRANCH" "$TASK_SLUG")
+ AGENT_BRANCH="agent/agent-${TASK_ID}"
+
+ echo "✅ Worktree created:"
+ echo " Directory: $WORK_DIR"
+ echo " Branch: $AGENT_BRANCH"
+ echo " Base: $CURRENT_BRANCH"
+ echo ""
+else
+ WORK_DIR=$(pwd)
+ AGENT_BRANCH=""
+fi
+
+echo "🚀 Spawning Claude agent in tmux session..."
+echo ""
+
+# Generate handover if requested
+HANDOVER_CONTENT=""
+if [ "$WITH_HANDOVER" = true ]; then
+ echo "📝 Generating handover context..."
+
+ # Get current branch and recent commits
+ CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
+ RECENT_COMMITS=$(git log --oneline -5 2>/dev/null || echo "No git history")
+ GIT_STATUS=$(git status -sb 2>/dev/null || echo "Not a git repo")
+
+ # Create handover content
+ HANDOVER_CONTENT=$(cat << EOF
+
+# Handover Context
+
+## Current State
+- Branch: $CURRENT_BRANCH
+- Directory: $WORK_DIR
+- Time: $(date)
+
+## Recent Commits
+$RECENT_COMMITS
+
+## Git Status
+$GIT_STATUS
+
+## Your Task
+$TASK
+
+---
+Please review the above context and proceed with the task.
+EOF
+)
+
+ echo "✅ Handover generated"
+ echo ""
+fi
+
+# Create tmux session
+tmux new-session -d -s "$SESSION" -c "$WORK_DIR"
+
+# Verify session creation
+if ! tmux has-session -t "$SESSION" 2>/dev/null; then
+ echo "❌ Failed to create tmux session"
+ exit 1
+fi
+
+echo "✅ Created tmux session: $SESSION"
+echo ""
+
+# Start Claude Code in the session
+tmux send-keys -t "$SESSION" "claude --dangerously-skip-permissions" C-m
+
+# Wait for Claude to be ready (not just sleep!)
+if ! wait_for_claude_ready "$SESSION"; then
+ echo "❌ Failed to start Claude agent - cleaning up..."
+ tmux kill-session -t "$SESSION" 2>/dev/null
+ exit 1
+fi
+
+# Additional small delay for UI stabilization
+sleep 0.5
+
+# Send handover context if generated (line-by-line to handle newlines)
+if [ "$WITH_HANDOVER" = true ]; then
+ echo "📤 Sending handover context to agent..."
+
+ # Send line-by-line to handle multi-line content properly
+ echo "$HANDOVER_CONTENT" | while IFS= read -r LINE || [ -n "$LINE" ]; do
+ # Use -l flag to send literal text (handles special characters)
+ tmux send-keys -t "$SESSION" -l "$LINE"
+ tmux send-keys -t "$SESSION" C-m
+ sleep 0.05 # Small delay between lines
+ done
+
+ # Final Enter to submit
+ tmux send-keys -t "$SESSION" C-m
+ sleep 0.5
+fi
+
+# Send the task (use literal mode for safety with special characters)
+echo "📤 Sending task to agent..."
+tmux send-keys -t "$SESSION" -l "$TASK"
+tmux send-keys -t "$SESSION" C-m
+
+# Small delay for Claude to start processing
+sleep 1
+
+# Verify task was received by checking if Claude is processing
+CURRENT_OUTPUT=$(tmux capture-pane -t "$SESSION" -p 2>/dev/null)
+if echo "$CURRENT_OUTPUT" | grep -qE "Thought for|Forming|Creating|Implement|⏳|✽|∴"; then
+ echo "✅ Task received and processing"
+elif echo "$CURRENT_OUTPUT" | grep -qE "error|failed|crash"; then
+ echo "⚠️ Warning: Detected error in agent output"
+ echo "📋 Last 10 lines of output:"
+ tmux capture-pane -t "$SESSION" -p | tail -10
+else
+ echo "ℹ️ Task sent (unable to confirm receipt - agent may still be starting)"
+fi
+
+echo ""
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo "✨ Agent spawned successfully!"
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+echo ""
+echo "Session: $SESSION"
+echo "Task: $TASK"
+echo "Directory: $WORK_DIR"
+echo ""
+echo "To monitor:"
+echo " tmux attach -t $SESSION"
+echo ""
+echo "To send more commands:"
+echo " tmux send-keys -t $SESSION \"your command\" C-m"
+echo ""
+echo "To kill session:"
+echo " tmux kill-session -t $SESSION"
+echo ""
+
+# Save metadata
+mkdir -p ~/.claude/agents
+cat > ~/.claude/agents/${SESSION}.json < /dev/null && echo "❌ adb not found. Is Android SDK installed?" && exit 1
+
+! emulator -list-avds 2>/dev/null | grep -q "^${DEVICE}$" && echo "❌ Emulator '$DEVICE' not found" && emulator -list-avds && exit 1
+
+RUNNING_EMULATOR=$(adb devices | grep "emulator" | cut -f1)
+
+if [ -z "$RUNNING_EMULATOR" ]; then
+ emulator -avd "$DEVICE" -no-snapshot-load -no-boot-anim &
+ adb wait-for-device
+ sleep 5
+ while [ "$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')" != "1" ]; do
+ sleep 2
+ done
+fi
+
+EMULATOR_SERIAL=$(adb devices | grep "emulator" | cut -f1 | head -1)
+```
+
+### Step 5: Setup Port Forwarding
+
+```bash
+# For dev server access from emulator
+if [ "$PROJECT_TYPE" = "react-native" ] || grep -q "\"dev\":" package.json 2>/dev/null; then
+ DEV_PORT=$(shuf -i 3000-9999 -n 1)
+ adb -s "$EMULATOR_SERIAL" reverse tcp:$DEV_PORT tcp:$DEV_PORT
+fi
+```
+
+### Step 6: Configure Poltergeist (Optional)
+
+```bash
+POLTERGEIST_AVAILABLE=false
+
+if command -v poltergeist &> /dev/null; then
+ POLTERGEIST_AVAILABLE=true
+
+ [ ! -f ".poltergeist.yml" ] && cat > .poltergeist.yml </dev/null; then
+ tmux new-window -t "$SESSION" -n dev-server
+ tmux send-keys -t "$SESSION:dev-server" "PORT=$DEV_PORT npm start | tee dev-server.log" C-m
+fi
+
+# Poltergeist (if available)
+if [ "$POLTERGEIST_AVAILABLE" = true ]; then
+ tmux new-window -t "$SESSION" -n poltergeist
+ tmux send-keys -t "$SESSION:poltergeist" "poltergeist watch --platform android | tee poltergeist.log" C-m
+fi
+
+# Logs
+tmux new-window -t "$SESSION" -n logs
+tmux send-keys -t "$SESSION:logs" "adb -s $EMULATOR_SERIAL logcat -v color" C-m
+
+# Git
+tmux new-window -t "$SESSION" -n git
+tmux send-keys -t "$SESSION:git" "git status" C-m
+```
+
+### Step 10: Save Metadata
+
+```bash
+cat > .tmux-android-session.json < /dev/null; then
+ POLTERGEIST_AVAILABLE=true
+
+ [ ! -f ".poltergeist.yml" ] && cat > .poltergeist.yml </dev/null; then
+ DEV_PORT=$(shuf -i 3000-9999 -n 1)
+ tmux new-window -t "$SESSION" -n dev-server
+ tmux send-keys -t "$SESSION:dev-server" "PORT=$DEV_PORT npm start | tee dev-server.log" C-m
+fi
+
+# Poltergeist (if available)
+if [ "$POLTERGEIST_AVAILABLE" = true ]; then
+ tmux new-window -t "$SESSION" -n poltergeist
+ tmux send-keys -t "$SESSION:poltergeist" "poltergeist watch --platform ios | tee poltergeist.log" C-m
+fi
+
+# Logs
+tmux new-window -t "$SESSION" -n logs
+tmux send-keys -t "$SESSION:logs" "xcrun simctl spawn $SIMULATOR_UDID log stream --level debug" C-m
+
+# Git
+tmux new-window -t "$SESSION" -n git
+tmux send-keys -t "$SESSION:git" "git status" C-m
+```
+
+### Step 9: Save Metadata
+
+```bash
+cat > .tmux-ios-session.json </dev/null
+ exit 1
+fi
+```
+
+### Step 2: Detect Project Type
+
+```bash
+detect_project_type() {
+ if [ -f "package.json" ]; then
+ grep -q "\"next\":" package.json && echo "nextjs" && return
+ grep -q "\"vite\":" package.json && echo "vite" && return
+ grep -q "\"react-scripts\":" package.json && echo "cra" && return
+ grep -q "\"@vue/cli\":" package.json && echo "vue" && return
+ echo "node"
+ elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
+ grep -q "django" requirements.txt pyproject.toml 2>/dev/null && echo "django" && return
+ grep -q "flask" requirements.txt pyproject.toml 2>/dev/null && echo "flask" && return
+ echo "python"
+ elif [ -f "Cargo.toml" ]; then
+ echo "rust"
+ elif [ -f "go.mod" ]; then
+ echo "go"
+ else
+ echo "unknown"
+ fi
+}
+
+PROJECT_TYPE=$(detect_project_type)
+```
+
+### Step 3: Detect Required Services
+
+```bash
+NEEDS_SUPABASE=false
+NEEDS_POSTGRES=false
+NEEDS_REDIS=false
+
+[ -f "supabase/config.toml" ] && NEEDS_SUPABASE=true
+grep -q "postgres" "$ENV_FILE" 2>/dev/null && NEEDS_POSTGRES=true
+grep -q "redis" "$ENV_FILE" 2>/dev/null && NEEDS_REDIS=true
+```
+
+### Step 4: Generate Random Port
+
+```bash
+DEV_PORT=$(shuf -i 3000-9999 -n 1)
+
+while lsof -i :$DEV_PORT >/dev/null 2>&1; do
+ DEV_PORT=$(shuf -i 3000-9999 -n 1)
+done
+```
+
+### Step 5: Create tmux Session
+
+```bash
+PROJECT_NAME=$(basename "$(pwd)")
+TIMESTAMP=$(date +%s)
+SESSION="dev-${PROJECT_NAME}-${TIMESTAMP}"
+
+tmux new-session -d -s "$SESSION" -n servers
+```
+
+### Step 6: Start Services
+
+```bash
+PANE_COUNT=0
+
+# Main dev server
+case $PROJECT_TYPE in
+ nextjs|vite|cra|vue)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "PORT=$DEV_PORT npm run dev | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+ django)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "python manage.py runserver $DEV_PORT | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+ flask)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "FLASK_RUN_PORT=$DEV_PORT flask run | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+ *)
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "PORT=$DEV_PORT npm run dev | tee dev-server-${DEV_PORT}.log" C-m
+ ;;
+esac
+
+# Additional services (if needed)
+if [ "$NEEDS_SUPABASE" = true ]; then
+ PANE_COUNT=$((PANE_COUNT + 1))
+ tmux split-window -v -t "$SESSION:servers"
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "supabase start" C-m
+fi
+
+if [ "$NEEDS_POSTGRES" = true ] && [ "$NEEDS_SUPABASE" = false ]; then
+ PANE_COUNT=$((PANE_COUNT + 1))
+ tmux split-window -v -t "$SESSION:servers"
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "docker-compose up postgres" C-m
+fi
+
+if [ "$NEEDS_REDIS" = true ]; then
+ PANE_COUNT=$((PANE_COUNT + 1))
+ tmux split-window -v -t "$SESSION:servers"
+ tmux send-keys -t "$SESSION:servers.${PANE_COUNT}" "redis-server" C-m
+fi
+
+tmux select-layout -t "$SESSION:servers" tiled
+```
+
+### Step 7: Create Additional Windows
+
+```bash
+# Logs window
+tmux new-window -t "$SESSION" -n logs
+tmux send-keys -t "$SESSION:logs" "tail -f dev-server-${DEV_PORT}.log 2>/dev/null || sleep infinity" C-m
+
+# Work window
+tmux new-window -t "$SESSION" -n work
+
+# Git window
+tmux new-window -t "$SESSION" -n git
+tmux send-keys -t "$SESSION:git" "git status" C-m
+```
+
+### Step 8: Save Metadata
+
+```bash
+cat > .tmux-dev-session.json <
+```
+
+**Long-running detached sessions**:
+```
+💡 Found dev sessions running >2 hours
+Recommendation: Check if still needed: tmux attach -t
+```
+
+**Many sessions (>5)**:
+```
+🧹 Found 5+ active sessions
+Recommendation: Review and clean up unused sessions
+```
+
+## Use Cases
+
+### Before Starting New Environment
+
+```bash
+/tmux-status
+# Check for port conflicts and existing sessions before /start-local
+```
+
+### Monitor Agent Progress
+
+```bash
+/tmux-status
+# See status of spawned agents (running, completed, etc.)
+```
+
+### Session Discovery
+
+```bash
+/tmux-status --detailed
+# Find specific session by project name or port
+```
+
+## Notes
+
+- Read-only, never modifies sessions
+- Uses tmux-monitor skill for discovery
+- Integrates with tmuxwatch if available
+- Detects metadata from `.tmux-dev-session.json` and `~/.claude/agents/*.json`
diff --git a/claude-code/orchestration/state/config.json b/claude-code/orchestration/state/config.json
new file mode 100644
index 0000000..32484e6
--- /dev/null
+++ b/claude-code/orchestration/state/config.json
@@ -0,0 +1,24 @@
+{
+ "orchestrator": {
+ "max_concurrent_agents": 4,
+ "idle_timeout_minutes": 15,
+ "checkpoint_interval_minutes": 5,
+ "max_retry_attempts": 3,
+ "polling_interval_seconds": 30
+ },
+ "merge": {
+ "default_strategy": "sequential",
+ "require_tests": true,
+ "auto_merge": false
+ },
+ "monitoring": {
+ "check_interval_seconds": 30,
+ "log_level": "info",
+ "enable_cost_tracking": true
+ },
+ "resource_limits": {
+ "max_budget_usd": 50,
+ "warn_at_percent": 80,
+ "hard_stop_at_percent": 100
+ }
+}
diff --git a/claude-code/skills/frontend-design/SKILL.md b/claude-code/skills/frontend-design/SKILL.md
new file mode 100644
index 0000000..a928b72
--- /dev/null
+++ b/claude-code/skills/frontend-design/SKILL.md
@@ -0,0 +1,145 @@
+---
+name: frontend-design
+description: Frontend design skill for UI/UX implementation - generates distinctive, production-grade interfaces
+version: 1.0.0
+authors:
+ - Prithvi Rajasekaran
+ - Alexander Bricken
+---
+
+# Frontend Design Skill
+
+This skill helps create **distinctive, production-grade frontend interfaces** that avoid generic AI aesthetics.
+
+## Core Principles
+
+When building any frontend interface, follow these principles to create visually striking, memorable designs:
+
+### 1. Establish Bold Aesthetic Direction
+
+**Before writing any code**, define a clear aesthetic vision:
+
+- **Understand the purpose**: What is this interface trying to achieve?
+- **Choose an extreme tone**: Select a distinctive aesthetic direction
+ - Brutalist: Raw, bold, functional
+ - Maximalist: Rich, layered, decorative
+ - Retro-futuristic: Nostalgic tech aesthetics
+ - Minimalist with impact: Powerful simplicity
+ - Neo-brutalist: Modern take on brutalism
+- **Identify the unforgettable element**: What will make this design memorable?
+
+### 2. Implementation Standards
+
+Every interface you create should be:
+
+- ✅ **Production-grade and functional**: Code that works flawlessly
+- ✅ **Visually striking and memorable**: Designs that stand out
+- ✅ **Cohesive with clear aesthetic point-of-view**: Unified vision throughout
+
+## Critical Design Guidelines
+
+### Typography
+
+**Choose fonts that are beautiful, unique, and interesting.**
+
+- ❌ **AVOID**: Generic system fonts (Arial, Helvetica, default sans-serif)
+- ✅ **USE**: Distinctive choices that elevate aesthetics
+ - Display fonts with character
+ - Unexpected font pairings
+ - Variable fonts for dynamic expression
+ - Fonts that reinforce your aesthetic direction
+
+### Color & Theme
+
+**Commit to cohesive aesthetics with CSS variables.**
+
+- ❌ **AVOID**: Generic color palettes, predictable combinations
+- ✅ **USE**: Dominant colors with sharp accents
+ - Define comprehensive CSS custom properties
+ - Create mood through color temperature
+ - Use unexpected color combinations
+ - Build depth with tints, shades, and tones
+
+### Motion & Animation
+
+**Use high-impact animations that enhance the experience.**
+
+- For **HTML/CSS**: CSS-only animations (transforms, transitions, keyframes)
+- For **React**: Motion library (Framer Motion, React Spring)
+- ❌ **AVOID**: Generic fade-ins, boring transitions
+- ✅ **USE**: High-impact moments
+ - Purposeful movement that guides attention
+ - Smooth, performant animations
+ - Delightful micro-interactions
+ - Entrance/exit animations with personality
+
+### Composition & Layout
+
+**Embrace unexpected layouts.**
+
+- ❌ **AVOID**: Predictable grids, centered everything, safe layouts
+- ✅ **USE**: Bold composition choices
+ - Asymmetry
+ - Overlap
+ - Diagonal flow
+ - Unexpected whitespace
+ - Breaking the grid intentionally
+
+### Details & Atmosphere
+
+**Create atmosphere through thoughtful details.**
+
+- ✅ Textures and grain
+- ✅ Sophisticated gradients
+- ✅ Patterns and backgrounds
+- ✅ Custom effects (blur, glow, shadows)
+- ✅ Attention to spacing and rhythm
+
+## What to AVOID
+
+**Generic AI Design Patterns:**
+
+- ❌ Overused fonts (Inter, Roboto, Open Sans as defaults)
+- ❌ Clichéd color schemes (purple gradients, generic blues)
+- ❌ Predictable layouts (everything centered, safe grids)
+- ❌ Cookie-cutter design that lacks context-specific character
+- ❌ Lack of personality or point-of-view
+- ❌ Generic animations (basic fade-ins everywhere)
+
+## Execution Philosophy
+
+**Show restraint or elaboration as the vision demands—execution quality matters most.**
+
+- Every design decision should serve the aesthetic direction
+- Don't add complexity for its own sake
+- Don't oversimplify when richness is needed
+- Commit fully to your chosen direction
+- Polish details relentlessly
+
+## Implementation Process
+
+When creating a frontend interface:
+
+1. **Define the aesthetic direction** (brutalist, maximalist, minimalist, etc.)
+2. **Choose distinctive typography** that reinforces the aesthetic
+3. **Establish color system** with CSS variables
+4. **Design layout** with unexpected but purposeful composition
+5. **Add motion** that enhances key moments
+6. **Polish details** (textures, shadows, spacing)
+7. **Review against principles** - is this distinctive and production-grade?
+
+## Examples of Strong Aesthetic Directions
+
+- **Brutalist Dashboard**: Monospace fonts, high contrast, grid-based, utilitarian
+- **Retro-Futuristic Landing**: Neon colors, chrome effects, 80s sci-fi inspired
+- **Minimalist with Impact**: Generous whitespace, bold typography, single accent color
+- **Neo-Brutalist App**: Raw aesthetics, asymmetric layouts, bold shadows
+- **Maximalist Content**: Rich layers, decorative elements, abundant color
+
+## Resources
+
+For deeper guidance on prompting for high-quality frontend design, see the [Frontend Aesthetics Cookbook](https://github.com/anthropics/claude-cookbooks/blob/main/coding/prompting_for_frontend_aesthetics.ipynb).
+
+---
+
+**Remember**: The goal is to create interfaces that are both functionally excellent and visually unforgettable. Avoid generic AI aesthetics by committing to a clear, bold direction and executing it with meticulous attention to detail.
diff --git a/claude-code/skills/tmux-monitor/SKILL.md b/claude-code/skills/tmux-monitor/SKILL.md
new file mode 100644
index 0000000..42cb23c
--- /dev/null
+++ b/claude-code/skills/tmux-monitor/SKILL.md
@@ -0,0 +1,370 @@
+---
+name: tmux-monitor
+description: Monitor and report status of all tmux sessions including dev environments, spawned agents, and running processes. Uses tmuxwatch for enhanced visibility.
+version: 1.0.0
+---
+
+# tmux-monitor Skill
+
+## Purpose
+
+Provide comprehensive visibility into all active tmux sessions, running processes, and spawned agents. This skill enables checking what's running where without needing to manually inspect each session.
+
+## Capabilities
+
+1. **Session Discovery**: Find and categorize all tmux sessions
+2. **Process Inspection**: Identify running servers, dev environments, agents
+3. **Port Mapping**: Show which ports are in use and by what
+4. **Status Reporting**: Generate detailed reports with recommendations
+5. **tmuxwatch Integration**: Use tmuxwatch for enhanced real-time monitoring
+6. **Metadata Extraction**: Read session metadata from .tmux-dev-session.json and agent JSON files
+
+## When to Use
+
+- User asks "what's running?"
+- Before starting new dev environments (check port conflicts)
+- After spawning agents (verify they started correctly)
+- When debugging server/process issues
+- Before session cleanup
+- When context switching between projects
+
+## Implementation
+
+### Step 1: Check tmux Availability
+
+```bash
+if ! command -v tmux &> /dev/null; then
+ echo "❌ tmux is not installed"
+ exit 1
+fi
+
+if ! tmux list-sessions 2>/dev/null; then
+ echo "✅ No tmux sessions currently running"
+ exit 0
+fi
+```
+
+### Step 2: Discover All Sessions
+
+```bash
+# Get all sessions with metadata
+SESSIONS=$(tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}')
+
+# Count sessions
+TOTAL_SESSIONS=$(echo "$SESSIONS" | wc -l | tr -d ' ')
+```
+
+### Step 3: Categorize Sessions
+
+Group by prefix pattern:
+
+- `dev-*` → Development environments
+- `agent-*` → Spawned agents
+- `claude-*` → Claude Code sessions
+- `monitor-*` → Monitoring sessions
+- Others → Miscellaneous
+
+```bash
+DEV_SESSIONS=$(echo "$SESSIONS" | grep "^dev-" || true)
+AGENT_SESSIONS=$(echo "$SESSIONS" | grep "^agent-" || true)
+CLAUDE_SESSIONS=$(echo "$SESSIONS" | grep "^claude-" || true)
+```
+
+### Step 4: Extract Details for Each Session
+
+For each session, gather:
+
+**Window Information**:
+```bash
+tmux list-windows -t "$SESSION" -F '#{window_index}:#{window_name}:#{window_panes}'
+```
+
+**Running Processes** (from first pane of each window):
+```bash
+tmux capture-pane -t "$SESSION:0.0" -p -S -10 -E 0
+```
+
+**Port Detection** (check for listening ports):
+```bash
+# Extract ports from session metadata
+if [ -f ".tmux-dev-session.json" ]; then
+ BACKEND_PORT=$(jq -r '.backend.port // empty' .tmux-dev-session.json)
+ FRONTEND_PORT=$(jq -r '.frontend.port // empty' .tmux-dev-session.json)
+fi
+
+# Or detect from process list
+lsof -nP -iTCP -sTCP:LISTEN | grep -E "node|python|uv|npm"
+```
+
+### Step 5: Load Session Metadata
+
+**Dev Environment Metadata** (`.tmux-dev-session.json`):
+```bash
+if [ -f ".tmux-dev-session.json" ]; then
+ PROJECT=$(jq -r '.project' .tmux-dev-session.json)
+ TYPE=$(jq -r '.type' .tmux-dev-session.json)
+ BACKEND_PORT=$(jq -r '.backend.port // "N/A"' .tmux-dev-session.json)
+ FRONTEND_PORT=$(jq -r '.frontend.port // "N/A"' .tmux-dev-session.json)
+ CREATED=$(jq -r '.created' .tmux-dev-session.json)
+fi
+```
+
+**Agent Metadata** (`~/.claude/agents/*.json`):
+```bash
+if [ -f "$HOME/.claude/agents/${SESSION}.json" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type' "$HOME/.claude/agents/${SESSION}.json")
+ TASK=$(jq -r '.task' "$HOME/.claude/agents/${SESSION}.json")
+ STATUS=$(jq -r '.status' "$HOME/.claude/agents/${SESSION}.json")
+ DIRECTORY=$(jq -r '.directory' "$HOME/.claude/agents/${SESSION}.json")
+ CREATED=$(jq -r '.created' "$HOME/.claude/agents/${SESSION}.json")
+fi
+```
+
+### Step 6: tmuxwatch Integration
+
+If tmuxwatch is available, offer enhanced view:
+
+```bash
+if command -v tmuxwatch &> /dev/null; then
+ echo ""
+ echo "📊 Enhanced Monitoring Available:"
+ echo " Real-time TUI: tmuxwatch"
+ echo " JSON export: tmuxwatch --dump | jq"
+ echo ""
+
+ # Optional: Use tmuxwatch for structured data
+ TMUXWATCH_DATA=$(tmuxwatch --dump 2>/dev/null || echo "{}")
+fi
+```
+
+### Step 7: Generate Comprehensive Report
+
+```markdown
+# tmux Sessions Overview
+
+**Total Active Sessions**: {count}
+**Total Windows**: {window_count}
+**Total Panes**: {pane_count}
+
+---
+
+## Development Environments ({dev_count})
+
+### 1. dev-myapp-1705161234
+- **Type**: fullstack
+- **Project**: myapp
+- **Status**: ⚡ Active (attached)
+- **Windows**: 4 (servers, logs, claude-work, git)
+- **Panes**: 8
+- **Backend**: Port 8432 → http://localhost:8432
+- **Frontend**: Port 3891 → http://localhost:3891
+- **Created**: 2025-01-13 14:30:00 (2h ago)
+- **Attach**: `tmux attach -t dev-myapp-1705161234`
+
+---
+
+## Spawned Agents ({agent_count})
+
+### 2. agent-1705160000
+- **Agent Type**: codex
+- **Task**: Refactor authentication module
+- **Status**: ⚙️ Running (15 minutes)
+- **Working Directory**: /Users/stevie/projects/myapp
+- **Git Worktree**: worktrees/agent-1705160000
+- **Windows**: 1 (work)
+- **Panes**: 2 (agent | monitoring)
+- **Last Output**: "Analyzing auth.py dependencies..."
+- **Attach**: `tmux attach -t agent-1705160000`
+- **Metadata**: `~/.claude/agents/agent-1705160000.json`
+
+### 3. agent-1705161000
+- **Agent Type**: aider
+- **Task**: Generate API documentation
+- **Status**: ✅ Completed (5 minutes ago)
+- **Output**: Documentation written to docs/api/
+- **Attach**: `tmux attach -t agent-1705161000` (review)
+- **Cleanup**: `tmux kill-session -t agent-1705161000`
+
+---
+
+## Running Processes Summary
+
+| Port | Service | Session | Status |
+|------|--------------|--------------------------|---------|
+| 8432 | Backend API | dev-myapp-1705161234 | Running |
+| 3891 | Frontend Dev | dev-myapp-1705161234 | Running |
+| 5160 | Supabase | dev-shotclubhouse-xxx | Running |
+
+---
+
+## Quick Actions
+
+**Attach to session**:
+```bash
+tmux attach -t
+```
+
+**Kill session**:
+```bash
+tmux kill-session -t
+```
+
+**List all sessions**:
+```bash
+tmux ls
+```
+
+**Kill all completed agents**:
+```bash
+for session in $(tmux ls | grep "^agent-" | cut -d: -f1); do
+ STATUS=$(jq -r '.status' "$HOME/.claude/agents/${session}.json" 2>/dev/null)
+ if [ "$STATUS" = "completed" ]; then
+ tmux kill-session -t "$session"
+ fi
+done
+```
+
+---
+
+## Recommendations
+
+{generated based on findings}
+```
+
+### Step 8: Provide Contextual Recommendations
+
+**If completed agents found**:
+```
+⚠️ Found 1 completed agent session:
+ - agent-1705161000: Task completed 5 minutes ago
+
+Recommendation: Review results and clean up:
+ tmux attach -t agent-1705161000 # Review
+ tmux kill-session -t agent-1705161000 # Cleanup
+```
+
+**If long-running detached sessions**:
+```
+💡 Found detached session running for 2h 40m:
+ - dev-api-service-1705159000
+
+Recommendation: Check if still needed:
+ tmux attach -t dev-api-service-1705159000
+```
+
+**If port conflicts detected**:
+```
+⚠️ Port conflict detected:
+ - Port 3000 in use by dev-oldproject-xxx
+ - New session will use random port instead
+
+Recommendation: Clean up old session if no longer needed
+```
+
+## Output Formats
+
+### Compact (Default)
+
+```
+5 active sessions:
+- dev-myapp-1705161234 (fullstack, 4 windows, active)
+- dev-api-service-1705159000 (backend-only, 4 windows, detached)
+- agent-1705160000 (codex, running 15m)
+- agent-1705161000 (aider, completed ✓)
+- claude-work (main session, current)
+
+3 running servers:
+- Port 8432: Backend API (dev-myapp)
+- Port 3891: Frontend Dev (dev-myapp)
+- Port 5160: Supabase (dev-shotclubhouse)
+```
+
+### Detailed (Verbose)
+
+Full report with all metadata, sample output, recommendations.
+
+### JSON (Programmatic)
+
+```json
+{
+ "sessions": [
+ {
+ "name": "dev-myapp-1705161234",
+ "type": "dev-environment",
+ "category": "fullstack",
+ "windows": 4,
+ "panes": 8,
+ "status": "attached",
+ "created": "2025-01-13T14:30:00Z",
+ "ports": {
+ "backend": 8432,
+ "frontend": 3891
+ },
+ "metadata_file": ".tmux-dev-session.json"
+ },
+ {
+ "name": "agent-1705160000",
+ "type": "spawned-agent",
+ "agent_type": "codex",
+ "task": "Refactor authentication module",
+ "status": "running",
+ "runtime": "15m",
+ "directory": "/Users/stevie/projects/myapp",
+ "worktree": "worktrees/agent-1705160000",
+ "metadata_file": "~/.claude/agents/agent-1705160000.json"
+ }
+ ],
+ "summary": {
+ "total_sessions": 5,
+ "total_windows": 12,
+ "total_panes": 28,
+ "running_servers": 3,
+ "active_agents": 1,
+ "completed_agents": 1
+ },
+ "ports": [
+ {"port": 8432, "service": "Backend API", "session": "dev-myapp-1705161234"},
+ {"port": 3891, "service": "Frontend Dev", "session": "dev-myapp-1705161234"},
+ {"port": 5160, "service": "Supabase", "session": "dev-shotclubhouse-xxx"}
+ ]
+}
+```
+
+## Integration with Commands
+
+This skill is used by:
+- `/tmux-status` command (user-facing command)
+- Automatically before starting new dev environments (conflict detection)
+- By spawned agents to check session status
+
+## Dependencies
+
+- `tmux` (required)
+- `jq` (required for JSON parsing)
+- `lsof` (optional, for port detection)
+- `tmuxwatch` (optional, for enhanced monitoring)
+
+## File Structure
+
+```
+~/.claude/agents/
+ agent-{timestamp}.json # Agent metadata
+
+.tmux-dev-session.json # Dev environment metadata (per project)
+
+/tmp/tmux-monitor-cache.json # Optional cache for performance
+```
+
+## Related Commands
+
+- `/tmux-status` - User-facing wrapper around this skill
+- `/spawn-agent` - Creates sessions that this skill monitors
+- `/start-local`, `/start-ios`, `/start-android` - Create dev environments
+
+## Notes
+
+- This skill is read-only, never modifies sessions
+- Safe to run anytime without side effects
+- Provides snapshot of current state
+- Can be cached for performance (TTL: 10 seconds)
+- Should be run before potentially conflicting operations
diff --git a/claude-code/skills/tmux-monitor/scripts/monitor.sh b/claude-code/skills/tmux-monitor/scripts/monitor.sh
new file mode 100755
index 0000000..0b1d3f5
--- /dev/null
+++ b/claude-code/skills/tmux-monitor/scripts/monitor.sh
@@ -0,0 +1,417 @@
+#!/bin/bash
+
+# ABOUTME: tmux session monitoring script - discovers, categorizes, and reports status of all active tmux sessions
+
+set -euo pipefail
+
+# Output mode: compact (default), detailed, json
+OUTPUT_MODE="${1:-compact}"
+
+# Check if tmux is available
+if ! command -v tmux &> /dev/null; then
+ echo "❌ tmux is not installed"
+ exit 1
+fi
+
+# Check if there are any sessions
+if ! tmux list-sessions 2>/dev/null | grep -q .; then
+ if [ "$OUTPUT_MODE" = "json" ]; then
+ echo '{"sessions": [], "summary": {"total_sessions": 0, "total_windows": 0, "total_panes": 0}}'
+ else
+ echo "✅ No tmux sessions currently running"
+ fi
+ exit 0
+fi
+
+# Initialize counters
+TOTAL_SESSIONS=0
+TOTAL_WINDOWS=0
+TOTAL_PANES=0
+
+# Arrays to store sessions by category
+declare -a DEV_SESSIONS
+declare -a AGENT_SESSIONS
+declare -a MONITOR_SESSIONS
+declare -a CLAUDE_SESSIONS
+declare -a OTHER_SESSIONS
+
+# Get all sessions
+SESSIONS=$(tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}' 2>/dev/null)
+
+# Parse and categorize sessions
+while IFS='|' read -r SESSION_NAME WINDOW_COUNT CREATED ATTACHED; do
+ TOTAL_SESSIONS=$((TOTAL_SESSIONS + 1))
+ TOTAL_WINDOWS=$((TOTAL_WINDOWS + WINDOW_COUNT))
+
+ # Get pane count for this session
+ PANE_COUNT=$(tmux list-panes -t "$SESSION_NAME" 2>/dev/null | wc -l | tr -d ' ')
+ TOTAL_PANES=$((TOTAL_PANES + PANE_COUNT))
+
+ # Categorize by prefix
+ if [[ "$SESSION_NAME" == dev-* ]]; then
+ DEV_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ elif [[ "$SESSION_NAME" == agent-* ]]; then
+ AGENT_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ elif [[ "$SESSION_NAME" == monitor-* ]]; then
+ MONITOR_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ elif [[ "$SESSION_NAME" == claude-* ]] || [[ "$SESSION_NAME" == *claude* ]]; then
+ CLAUDE_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ else
+ OTHER_SESSIONS+=("$SESSION_NAME|$WINDOW_COUNT|$PANE_COUNT|$ATTACHED")
+ fi
+done <<< "$SESSIONS"
+
+# Helper function to get session metadata
+get_dev_metadata() {
+ local SESSION_NAME=$1
+ local METADATA_FILE=".tmux-dev-session.json"
+
+ if [ -f "$METADATA_FILE" ]; then
+ local SESSION_IN_FILE=$(jq -r '.session // empty' "$METADATA_FILE" 2>/dev/null)
+ if [ "$SESSION_IN_FILE" = "$SESSION_NAME" ]; then
+ echo "$METADATA_FILE"
+ fi
+ fi
+
+ # Try iOS-specific metadata
+ if [ -f ".tmux-ios-session.json" ]; then
+ local SESSION_IN_FILE=$(jq -r '.session // empty' ".tmux-ios-session.json" 2>/dev/null)
+ if [ "$SESSION_IN_FILE" = "$SESSION_NAME" ]; then
+ echo ".tmux-ios-session.json"
+ fi
+ fi
+
+ # Try Android-specific metadata
+ if [ -f ".tmux-android-session.json" ]; then
+ local SESSION_IN_FILE=$(jq -r '.session // empty' ".tmux-android-session.json" 2>/dev/null)
+ if [ "$SESSION_IN_FILE" = "$SESSION_NAME" ]; then
+ echo ".tmux-android-session.json"
+ fi
+ fi
+}
+
+get_agent_metadata() {
+ local SESSION_NAME=$1
+ local METADATA_FILE="$HOME/.claude/agents/${SESSION_NAME}.json"
+
+ if [ -f "$METADATA_FILE" ]; then
+ echo "$METADATA_FILE"
+ fi
+}
+
+# Get running ports
+get_running_ports() {
+ if command -v lsof &> /dev/null; then
+ lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -E "node|python|uv|npm|ruby|java" | awk '{print $9}' | cut -d':' -f2 | sort -u || true
+ fi
+}
+
+RUNNING_PORTS=$(get_running_ports)
+
+# Output functions
+
+output_compact() {
+ echo "${TOTAL_SESSIONS} active sessions:"
+
+ # Dev environments
+ if [[ -v DEV_SESSIONS[@] ]] && [ ${#DEV_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${DEV_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="detached"
+ [ "$ATTACHED" = "1" ] && STATUS="active"
+
+ # Try to get metadata
+ METADATA_FILE=$(get_dev_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ PROJECT_TYPE=$(jq -r '.type // "dev"' "$METADATA_FILE" 2>/dev/null)
+ echo "- $SESSION_NAME ($PROJECT_TYPE, $WINDOW_COUNT windows, $STATUS)"
+ else
+ echo "- $SESSION_NAME ($WINDOW_COUNT windows, $STATUS)"
+ fi
+ done
+ fi
+
+ # Agent sessions
+ if [[ -v AGENT_SESSIONS[@] ]] && [ ${#AGENT_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${AGENT_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ # Try to get agent metadata
+ METADATA_FILE=$(get_agent_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ STATUS_=$(jq -r '.status // "running"' "$METADATA_FILE" 2>/dev/null)
+ echo "- $SESSION_NAME ($AGENT_TYPE, $STATUS_)"
+ else
+ echo "- $SESSION_NAME (agent)"
+ fi
+ done
+ fi
+
+ # Claude sessions
+ if [[ -v CLAUDE_SESSIONS[@] ]] && [ ${#CLAUDE_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${CLAUDE_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="detached"
+ [ "$ATTACHED" = "1" ] && STATUS="current"
+ echo "- $SESSION_NAME (main session, $STATUS)"
+ done
+ fi
+
+ # Other sessions
+ if [[ -v OTHER_SESSIONS[@] ]] && [ ${#OTHER_SESSIONS[@]} -gt 0 ]; then
+ for session_data in "${OTHER_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ echo "- $SESSION_NAME ($WINDOW_COUNT windows)"
+ done
+ fi
+
+ # Port summary
+ if [ -n "$RUNNING_PORTS" ]; then
+ PORT_COUNT=$(echo "$RUNNING_PORTS" | wc -l | tr -d ' ')
+ echo ""
+ echo "$PORT_COUNT running servers on ports: $(echo $RUNNING_PORTS | tr '\n' ',' | sed 's/,$//')"
+ fi
+
+ echo ""
+ echo "Use /tmux-status --detailed for full report"
+}
+
+output_detailed() {
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "📊 tmux Sessions Overview"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "**Total Active Sessions**: $TOTAL_SESSIONS"
+ echo "**Total Windows**: $TOTAL_WINDOWS"
+ echo "**Total Panes**: $TOTAL_PANES"
+ echo ""
+
+ # Dev environments
+ if [[ -v DEV_SESSIONS[@] ]] && [ ${#DEV_SESSIONS[@]} -gt 0 ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Development Environments (${#DEV_SESSIONS[@]})"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ local INDEX=1
+ for session_data in "${DEV_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="🔌 Detached"
+ [ "$ATTACHED" = "1" ] && STATUS="⚡ Active (attached)"
+
+ echo "### $INDEX. $SESSION_NAME"
+ echo "- **Status**: $STATUS"
+ echo "- **Windows**: $WINDOW_COUNT"
+ echo "- **Panes**: $PANE_COUNT"
+
+ # Get metadata if available
+ METADATA_FILE=$(get_dev_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ PROJECT=$(jq -r '.project // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ PROJECT_TYPE=$(jq -r '.type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ CREATED=$(jq -r '.created // "unknown"' "$METADATA_FILE" 2>/dev/null)
+
+ echo "- **Project**: $PROJECT ($PROJECT_TYPE)"
+ echo "- **Created**: $CREATED"
+
+ # Check for ports
+ if jq -e '.dev_port' "$METADATA_FILE" &>/dev/null; then
+ DEV_PORT=$(jq -r '.dev_port' "$METADATA_FILE" 2>/dev/null)
+ echo "- **Dev Server**: http://localhost:$DEV_PORT"
+ fi
+
+ if jq -e '.services' "$METADATA_FILE" &>/dev/null; then
+ echo "- **Services**: $(jq -r '.services | keys | join(", ")' "$METADATA_FILE" 2>/dev/null)"
+ fi
+ fi
+
+ echo "- **Attach**: \`tmux attach -t $SESSION_NAME\`"
+ echo ""
+
+ INDEX=$((INDEX + 1))
+ done
+ fi
+
+ # Agent sessions
+ if [[ -v AGENT_SESSIONS[@] ]] && [ ${#AGENT_SESSIONS[@]} -gt 0 ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Spawned Agents (${#AGENT_SESSIONS[@]})"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ local INDEX=1
+ for session_data in "${AGENT_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ echo "### $INDEX. $SESSION_NAME"
+
+ # Get agent metadata
+ METADATA_FILE=$(get_agent_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ TASK=$(jq -r '.task // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ STATUS_=$(jq -r '.status // "running"' "$METADATA_FILE" 2>/dev/null)
+ DIRECTORY=$(jq -r '.directory // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ CREATED=$(jq -r '.created // "unknown"' "$METADATA_FILE" 2>/dev/null)
+
+ echo "- **Agent Type**: $AGENT_TYPE"
+ echo "- **Task**: $TASK"
+ echo "- **Status**: $([ "$STATUS_" = "completed" ] && echo "✅ Completed" || echo "⚙️ Running")"
+ echo "- **Working Directory**: $DIRECTORY"
+ echo "- **Created**: $CREATED"
+
+ # Check for worktree
+ if jq -e '.worktree' "$METADATA_FILE" &>/dev/null; then
+ WORKTREE=$(jq -r '.worktree' "$METADATA_FILE" 2>/dev/null)
+ if [ "$WORKTREE" = "true" ]; then
+ AGENT_BRANCH=$(jq -r '.agent_branch // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ echo "- **Git Worktree**: Yes (branch: $AGENT_BRANCH)"
+ fi
+ fi
+ fi
+
+ echo "- **Attach**: \`tmux attach -t $SESSION_NAME\`"
+ echo "- **Metadata**: \`cat $METADATA_FILE\`"
+ echo ""
+
+ INDEX=$((INDEX + 1))
+ done
+ fi
+
+ # Claude sessions
+ if [[ -v CLAUDE_SESSIONS[@] ]] && [ ${#CLAUDE_SESSIONS[@]} -gt 0 ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Other Sessions (${#CLAUDE_SESSIONS[@]})"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+
+ for session_data in "${CLAUDE_SESSIONS[@]}"; do
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+ STATUS="Detached"
+ [ "$ATTACHED" = "1" ] && STATUS="⚡ Active (current session)"
+ echo "- $SESSION_NAME: $STATUS"
+ done
+ echo ""
+ fi
+
+ # Running processes summary
+ if [ -n "$RUNNING_PORTS" ]; then
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Running Processes Summary"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "| Port | Service | Status |"
+ echo "|------|---------|--------|"
+ for PORT in $RUNNING_PORTS; do
+ echo "| $PORT | Running | ✅ |"
+ done
+ echo ""
+ fi
+
+ # Quick actions
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo "## Quick Actions"
+ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+ echo ""
+ echo "**List all sessions**:"
+ echo "\`\`\`bash"
+ echo "tmux ls"
+ echo "\`\`\`"
+ echo ""
+ echo "**Attach to session**:"
+ echo "\`\`\`bash"
+ echo "tmux attach -t "
+ echo "\`\`\`"
+ echo ""
+ echo "**Kill session**:"
+ echo "\`\`\`bash"
+ echo "tmux kill-session -t "
+ echo "\`\`\`"
+ echo ""
+}
+
+output_json() {
+ echo "{"
+ echo " \"sessions\": ["
+
+ local FIRST_SESSION=true
+
+ # Dev sessions
+ for session_data in "${DEV_SESSIONS[@]}"; do
+ [ "$FIRST_SESSION" = false ] && echo ","
+ FIRST_SESSION=false
+
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ echo " {"
+ echo " \"name\": \"$SESSION_NAME\","
+ echo " \"type\": \"dev-environment\","
+ echo " \"windows\": $WINDOW_COUNT,"
+ echo " \"panes\": $PANE_COUNT,"
+ echo " \"attached\": $([ "$ATTACHED" = "1" ] && echo "true" || echo "false")"
+
+ # Get metadata if available
+ METADATA_FILE=$(get_dev_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ echo " ,\"metadata_file\": \"$METADATA_FILE\""
+ fi
+
+ echo -n " }"
+ done
+
+ # Agent sessions
+ for session_data in "${AGENT_SESSIONS[@]}"; do
+ [ "$FIRST_SESSION" = false ] && echo ","
+ FIRST_SESSION=false
+
+ IFS='|' read -r SESSION_NAME WINDOW_COUNT PANE_COUNT ATTACHED <<< "$session_data"
+
+ echo " {"
+ echo " \"name\": \"$SESSION_NAME\","
+ echo " \"type\": \"spawned-agent\","
+ echo " \"windows\": $WINDOW_COUNT,"
+ echo " \"panes\": $PANE_COUNT"
+
+ # Get agent metadata
+ METADATA_FILE=$(get_agent_metadata "$SESSION_NAME")
+ if [ -n "$METADATA_FILE" ]; then
+ AGENT_TYPE=$(jq -r '.agent_type // "unknown"' "$METADATA_FILE" 2>/dev/null)
+ STATUS_=$(jq -r '.status // "running"' "$METADATA_FILE" 2>/dev/null)
+ echo " ,\"agent_type\": \"$AGENT_TYPE\","
+ echo " \"status\": \"$STATUS_\","
+ echo " \"metadata_file\": \"$METADATA_FILE\""
+ fi
+
+ echo -n " }"
+ done
+
+ echo ""
+ echo " ],"
+ echo " \"summary\": {"
+ echo " \"total_sessions\": $TOTAL_SESSIONS,"
+ echo " \"total_windows\": $TOTAL_WINDOWS,"
+ echo " \"total_panes\": $TOTAL_PANES,"
+ echo " \"dev_sessions\": ${#DEV_SESSIONS[@]},"
+ echo " \"agent_sessions\": ${#AGENT_SESSIONS[@]}"
+ echo " }"
+ echo "}"
+}
+
+# Main output
+case "$OUTPUT_MODE" in
+ compact)
+ output_compact
+ ;;
+ detailed)
+ output_detailed
+ ;;
+ json)
+ output_json
+ ;;
+ *)
+ echo "Unknown output mode: $OUTPUT_MODE"
+ echo "Usage: monitor.sh [compact|detailed|json]"
+ exit 1
+ ;;
+esac
diff --git a/claude-code/skills/webapp-testing/SKILL.md b/claude-code/skills/webapp-testing/SKILL.md
index 4726215..a882380 100644
--- a/claude-code/skills/webapp-testing/SKILL.md
+++ b/claude-code/skills/webapp-testing/SKILL.md
@@ -38,14 +38,14 @@ To start a server, run `--help` first, then use the helper:
**Single server:**
```bash
-python scripts/with_server.py --server "npm run dev" --port 5173 -- python your_automation.py
+python scripts/with_server.py --server "npm run dev" --port 3000 -- python your_automation.py
```
**Multiple servers (e.g., backend + frontend):**
```bash
python scripts/with_server.py \
- --server "cd backend && python server.py" --port 3000 \
- --server "cd frontend && npm run dev" --port 5173 \
+ --server "cd backend && python server.py" --port 8000 \
+ --server "cd frontend && npm run dev" --port 3000 \
-- python your_automation.py
```
@@ -53,10 +53,12 @@ To create an automation script, include only Playwright logic (servers are manag
```python
from playwright.sync_api import sync_playwright
+APP_PORT = 3000 # Match the port from --port argument
+
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
page = browser.new_page()
- page.goto('http://localhost:5173') # Server already running and ready
+ page.goto(f'http://localhost:{APP_PORT}') # Server already running and ready
page.wait_for_load_state('networkidle') # CRITICAL: Wait for JS to execute
# ... your automation logic
browser.close()
@@ -88,9 +90,184 @@ with sync_playwright() as p:
- Use descriptive selectors: `text=`, `role=`, CSS selectors, or IDs
- Add appropriate waits: `page.wait_for_selector()` or `page.wait_for_timeout()`
+## Utility Modules
+
+The skill now includes comprehensive utilities for common testing patterns:
+
+### UI Interactions (`utils/ui_interactions.py`)
+
+Handle common UI patterns automatically:
+
+```python
+from utils.ui_interactions import (
+ dismiss_cookie_banner,
+ dismiss_modal,
+ click_with_header_offset,
+ force_click_if_needed,
+ wait_for_no_overlay,
+ wait_for_stable_dom
+)
+
+# Dismiss cookie consent
+dismiss_cookie_banner(page)
+
+# Close welcome modal
+dismiss_modal(page, modal_identifier="Welcome")
+
+# Click button behind fixed header
+click_with_header_offset(page, 'button#submit', header_height=100)
+
+# Try click with force fallback
+force_click_if_needed(page, 'button#action')
+
+# Wait for loading overlays to disappear
+wait_for_no_overlay(page)
+
+# Wait for DOM to stabilize
+wait_for_stable_dom(page)
+```
+
+### Smart Form Filling (`utils/form_helpers.py`)
+
+Intelligently handle form variations:
+
+```python
+from utils.form_helpers import (
+ SmartFormFiller,
+ handle_multi_step_form,
+ auto_fill_form
+)
+
+# Works with both "Full Name" and "First/Last Name" fields
+filler = SmartFormFiller()
+filler.fill_name_field(page, "Jane Doe")
+filler.fill_email_field(page, "jane@example.com")
+filler.fill_password_fields(page, "SecurePass123!")
+filler.fill_phone_field(page, "+447700900123")
+filler.fill_date_field(page, "1990-01-15", field_hint="birth")
+
+# Auto-fill entire form
+results = auto_fill_form(page, {
+ 'email': 'test@example.com',
+ 'password': 'Pass123!',
+ 'full_name': 'Test User',
+ 'phone': '+447700900123',
+ 'date_of_birth': '1990-01-15'
+})
+
+# Handle multi-step forms
+steps = [
+ {'fields': {'email': 'test@example.com', 'password': 'Pass123!'}, 'checkbox': True},
+ {'fields': {'full_name': 'Test User', 'date_of_birth': '1990-01-15'}},
+ {'complete': True}
+]
+handle_multi_step_form(page, steps)
+```
+
+### Supabase Testing (`utils/supabase.py`)
+
+Database operations for Supabase-based apps:
+
+```python
+from utils.supabase import SupabaseTestClient, quick_cleanup
+
+# Initialize client
+client = SupabaseTestClient(
+ url="https://project.supabase.co",
+ service_key="your-service-role-key",
+ db_password="your-db-password"
+)
+
+# Create test user
+user_id = client.create_user("test@example.com", "password123")
+
+# Create invite code
+client.create_invite_code("TEST2024", code_type="general")
+
+# Bypass email verification
+client.confirm_email(user_id)
+
+# Cleanup after test
+client.cleanup_related_records(user_id)
+client.delete_user(user_id)
+
+# Quick cleanup helper
+quick_cleanup("test@example.com", "db_password", "https://project.supabase.co")
+```
+
+### Advanced Wait Strategies (`utils/wait_strategies.py`)
+
+Better alternatives to simple sleep():
+
+```python
+from utils.wait_strategies import (
+ wait_for_api_call,
+ wait_for_element_stable,
+ smart_navigation_wait,
+ combined_wait
+)
+
+# Wait for specific API response
+response = wait_for_api_call(page, '**/api/profile**')
+
+# Wait for element to stop moving
+wait_for_element_stable(page, '.dropdown-menu', stability_ms=1000)
+
+# Smart navigation with URL check
+page.click('button#login')
+smart_navigation_wait(page, expected_url_pattern='**/dashboard**')
+
+# Comprehensive wait (network + DOM + overlays)
+combined_wait(page)
+```
+
+## Complete Examples
+
+### Multi-Step Registration
+
+See `examples/multi_step_registration.py` for a complete example showing:
+- Database setup (invite codes)
+- Cookie banner dismissal
+- Multi-step form automation
+- Email verification bypass
+- Login flow
+- Dashboard verification
+- Cleanup
+
+Run it:
+```bash
+python examples/multi_step_registration.py
+```
+
+## Using the Webapp-Testing Subagent
+
+A specialized subagent is available for testing automation. Use it to keep your main conversation focused on development:
+
+```
+You: "Use webapp-testing agent to register test@example.com and verify the parent role switch works"
+
+Main Agent: [Launches webapp-testing subagent]
+
+Webapp-Testing Agent: [Runs complete automation, returns results]
+```
+
+**Benefits:**
+- Keeps main context clean
+- Specialized for Playwright automation
+- Access to all skill utilities
+- Automatic screenshot capture
+- Clear result reporting
+
## Reference Files
- **examples/** - Examples showing common patterns:
- `element_discovery.py` - Discovering buttons, links, and inputs on a page
- `static_html_automation.py` - Using file:// URLs for local HTML
- - `console_logging.py` - Capturing console logs during automation
\ No newline at end of file
+ - `console_logging.py` - Capturing console logs during automation
+ - `multi_step_registration.py` - Complete registration flow example (NEW)
+
+- **utils/** - Reusable utility modules (NEW):
+ - `ui_interactions.py` - Cookie banners, modals, overlays, stable waits
+ - `form_helpers.py` - Smart form filling, multi-step automation
+ - `supabase.py` - Database operations for Supabase apps
+ - `wait_strategies.py` - Advanced waiting patterns
\ No newline at end of file
diff --git a/claude-code/skills/webapp-testing/examples/element_discovery.py b/claude-code/skills/webapp-testing/examples/element_discovery.py
index 917ba72..8ddc5af 100755
--- a/claude-code/skills/webapp-testing/examples/element_discovery.py
+++ b/claude-code/skills/webapp-testing/examples/element_discovery.py
@@ -7,7 +7,7 @@
page = browser.new_page()
# Navigate to page and wait for it to fully load
- page.goto('http://localhost:5173')
+ page.goto('http://localhost:3000') # Replace with your app URL
page.wait_for_load_state('networkidle')
# Discover all buttons on the page
diff --git a/claude-code/skills/webapp-testing/examples/multi_step_registration.py b/claude-code/skills/webapp-testing/examples/multi_step_registration.py
new file mode 100644
index 0000000..e960ff6
--- /dev/null
+++ b/claude-code/skills/webapp-testing/examples/multi_step_registration.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python3
+"""
+Multi-Step Registration Example
+
+Demonstrates complete registration flow using all webapp-testing utilities:
+- UI interactions (cookie banners, modals)
+- Smart form filling (handles field variations)
+- Database operations (invite codes, email verification)
+- Advanced wait strategies
+
+This example is based on a real-world React/Supabase app with 3-step registration.
+"""
+
+import sys
+import os
+from playwright.sync_api import sync_playwright
+import time
+
+# Add utils to path
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
+
+from utils.ui_interactions import dismiss_cookie_banner, dismiss_modal
+from utils.form_helpers import SmartFormFiller, handle_multi_step_form
+from utils.supabase import SupabaseTestClient
+from utils.wait_strategies import combined_wait, smart_navigation_wait
+
+
+def register_user_complete_flow():
+ """
+ Complete multi-step registration with database setup and verification.
+
+ Flow:
+ 1. Create invite code in database
+ 2. Navigate to registration page
+ 3. Fill multi-step form (Code → Credentials → Personal Info → Avatar)
+ 4. Verify email via database
+ 5. Login
+ 6. Verify dashboard access
+ 7. Cleanup (optional)
+ """
+
+ # Configuration - adjust for your app
+ APP_URL = "http://localhost:3000"
+ REGISTER_URL = f"{APP_URL}/register"
+
+ # Database config (adjust for your project)
+ DB_PASSWORD = "your-db-password"
+ SUPABASE_URL = "https://project.supabase.co"
+ SERVICE_KEY = "your-service-role-key"
+
+ # Test user data
+ TEST_EMAIL = "test.user@example.com"
+ TEST_PASSWORD = "TestPass123!"
+ FULL_NAME = "Test User"
+ PHONE = "+447700900123"
+ DATE_OF_BIRTH = "1990-01-15"
+ INVITE_CODE = "TEST2024"
+
+ print("\n" + "="*60)
+ print("MULTI-STEP REGISTRATION AUTOMATION")
+ print("="*60)
+
+ # Step 1: Setup database
+ print("\n[1/8] Setting up database...")
+ db_client = SupabaseTestClient(
+ url=SUPABASE_URL,
+ service_key=SERVICE_KEY,
+ db_password=DB_PASSWORD
+ )
+
+ # Create invite code
+ if db_client.create_invite_code(INVITE_CODE, code_type="general"):
+ print(f" ✓ Created invite code: {INVITE_CODE}")
+ else:
+ print(f" ⚠️ Invite code may already exist")
+
+ # Clean up any existing test user
+ existing_user = db_client.find_user_by_email(TEST_EMAIL)
+ if existing_user:
+ print(f" Cleaning up existing user...")
+ db_client.cleanup_related_records(existing_user)
+ db_client.delete_user(existing_user)
+
+ # Step 2: Start browser automation
+ print("\n[2/8] Starting browser automation...")
+
+ with sync_playwright() as p:
+ browser = p.chromium.launch(headless=False)
+ page = browser.new_page(viewport={'width': 1400, 'height': 1000})
+
+ try:
+ # Step 3: Navigate to registration
+ print("\n[3/8] Navigating to registration page...")
+ page.goto(REGISTER_URL, wait_until='networkidle')
+ time.sleep(2)
+
+ # Handle cookie banner
+ if dismiss_cookie_banner(page):
+ print(" ✓ Dismissed cookie banner")
+
+ page.screenshot(path='/tmp/reg_step1_start.png', full_page=True)
+ print(" ✓ Screenshot: /tmp/reg_step1_start.png")
+
+ # Step 4: Fill multi-step form
+ print("\n[4/8] Filling multi-step registration form...")
+
+ # Define form steps
+ steps = [
+ {
+ 'name': 'Invite Code',
+ 'fields': {'invite_code': INVITE_CODE},
+ 'custom_fill': lambda: page.locator('input').first.fill(INVITE_CODE),
+ 'custom_submit': lambda: page.locator('input').first.press('Enter'),
+ },
+ {
+ 'name': 'Credentials',
+ 'fields': {
+ 'email': TEST_EMAIL,
+ 'password': TEST_PASSWORD,
+ },
+ 'checkbox': True, # Terms of service
+ },
+ {
+ 'name': 'Personal Info',
+ 'fields': {
+ 'full_name': FULL_NAME,
+ 'date_of_birth': DATE_OF_BIRTH,
+ 'phone': PHONE,
+ },
+ },
+ {
+ 'name': 'Avatar Selection',
+ 'complete': True, # Final step with COMPLETE button
+ }
+ ]
+
+ # Process each step
+ filler = SmartFormFiller()
+
+ for i, step in enumerate(steps):
+ print(f"\n Step {i+1}/4: {step['name']}")
+
+ # Custom filling logic for first step (invite code)
+ if 'custom_fill' in step:
+ step['custom_fill']()
+ time.sleep(1)
+
+ if 'custom_submit' in step:
+ step['custom_submit']()
+ else:
+ page.locator('button:has-text("CONTINUE")').first.click()
+
+ time.sleep(4)
+ page.wait_for_load_state('networkidle')
+ time.sleep(2)
+
+ # Standard form filling for other steps
+ elif 'fields' in step:
+ if 'email' in step['fields']:
+ filler.fill_email_field(page, step['fields']['email'])
+ print(" ✓ Email")
+
+ if 'password' in step['fields']:
+ filler.fill_password_fields(page, step['fields']['password'])
+ print(" ✓ Password")
+
+ if 'full_name' in step['fields']:
+ filler.fill_name_field(page, step['fields']['full_name'])
+ print(" ✓ Full Name")
+
+ if 'date_of_birth' in step['fields']:
+ filler.fill_date_field(page, step['fields']['date_of_birth'], field_hint='birth')
+ print(" ✓ Date of Birth")
+
+ if 'phone' in step['fields']:
+ filler.fill_phone_field(page, step['fields']['phone'])
+ print(" ✓ Phone")
+
+ # Check terms checkbox if needed
+ if step.get('checkbox'):
+ page.locator('input[type="checkbox"]').first.check()
+ print(" ✓ Terms accepted")
+
+ time.sleep(1)
+
+ # Click continue
+ page.locator('button:has-text("CONTINUE")').first.click()
+ time.sleep(4)
+ page.wait_for_load_state('networkidle')
+ time.sleep(2)
+
+ # Final step - click COMPLETE
+ elif step.get('complete'):
+ complete_btn = page.locator('button:has-text("COMPLETE")').first
+ complete_btn.click()
+ print(" ✓ Clicked COMPLETE")
+
+ time.sleep(8)
+ page.wait_for_load_state('networkidle')
+ time.sleep(3)
+
+ # Screenshot after each step
+ page.screenshot(path=f'/tmp/reg_step{i+1}_complete.png', full_page=True)
+ print(f" ✓ Screenshot: /tmp/reg_step{i+1}_complete.png")
+
+ print("\n ✓ Multi-step form completed!")
+
+ # Step 5: Handle post-registration
+ print("\n[5/8] Handling post-registration...")
+
+ # Dismiss welcome modal if present
+ if dismiss_modal(page, modal_identifier="Welcome"):
+ print(" ✓ Dismissed welcome modal")
+
+ current_url = page.url
+ print(f" Current URL: {current_url}")
+
+ # Step 6: Verify email via database
+ print("\n[6/8] Verifying email via database...")
+ time.sleep(2) # Brief wait for user to be created in DB
+
+ user_id = db_client.find_user_by_email(TEST_EMAIL)
+ if user_id:
+ print(f" ✓ Found user: {user_id}")
+
+ if db_client.confirm_email(user_id):
+ print(" ✓ Email verified in database")
+ else:
+ print(" ⚠️ Could not verify email")
+ else:
+ print(" ⚠️ User not found in database")
+
+ # Step 7: Login (if not already logged in)
+ print("\n[7/8] Logging in...")
+
+ if 'login' in current_url.lower():
+ print(" Needs login...")
+
+ filler.fill_email_field(page, TEST_EMAIL)
+ filler.fill_password_fields(page, TEST_PASSWORD, confirm=False)
+ time.sleep(1)
+
+ page.locator('button[type="submit"]').first.click()
+ time.sleep(6)
+ page.wait_for_load_state('networkidle')
+ time.sleep(3)
+
+ print(" ✓ Logged in")
+ else:
+ print(" ✓ Already logged in")
+
+ # Step 8: Verify dashboard access
+ print("\n[8/8] Verifying dashboard access...")
+
+ # Navigate to dashboard/perform if not already there
+ if 'perform' not in page.url.lower() and 'dashboard' not in page.url.lower():
+ page.goto(f"{APP_URL}/perform", wait_until='networkidle')
+ time.sleep(3)
+
+ page.screenshot(path='/tmp/reg_final_dashboard.png', full_page=True)
+ print(" ✓ Screenshot: /tmp/reg_final_dashboard.png")
+
+ # Check if we're on the dashboard
+ if 'perform' in page.url.lower() or 'dashboard' in page.url.lower():
+ print(" ✓ Successfully reached dashboard!")
+ else:
+ print(f" ⚠️ Unexpected URL: {page.url}")
+
+ print("\n" + "="*60)
+ print("REGISTRATION COMPLETE!")
+ print("="*60)
+ print(f"\nUser: {TEST_EMAIL}")
+ print(f"Password: {TEST_PASSWORD}")
+ print(f"User ID: {user_id}")
+ print(f"\nScreenshots saved to /tmp/reg_step*.png")
+ print("="*60)
+
+ # Keep browser open for inspection
+ print("\nKeeping browser open for 30 seconds...")
+ time.sleep(30)
+
+ except Exception as e:
+ print(f"\n❌ Error: {e}")
+ import traceback
+ traceback.print_exc()
+ page.screenshot(path='/tmp/reg_error.png', full_page=True)
+ print(" Error screenshot: /tmp/reg_error.png")
+
+ finally:
+ browser.close()
+
+ # Optional cleanup
+ print("\n" + "="*60)
+ print("Cleanup")
+ print("="*60)
+
+ cleanup = input("\nDelete test user? (y/N): ").strip().lower()
+ if cleanup == 'y' and user_id:
+ print("Cleaning up...")
+ db_client.cleanup_related_records(user_id)
+ db_client.delete_user(user_id)
+ print("✓ Test user deleted")
+ else:
+ print("Test user kept for manual testing")
+
+
+if __name__ == '__main__':
+ print("\nMulti-Step Registration Automation Example")
+ print("=" * 60)
+ print("\nBefore running:")
+ print("1. Update configuration variables at the top of the script")
+ print("2. Ensure your app is running (e.g., npm run dev)")
+ print("3. Have database credentials ready")
+ print("\n" + "=" * 60)
+
+ proceed = input("\nProceed with registration? (y/N): ").strip().lower()
+
+ if proceed == 'y':
+ register_user_complete_flow()
+ else:
+ print("\nCancelled.")
diff --git a/claude-code/skills/webapp-testing/utils/__pycache__/form_helpers.cpython-312.pyc b/claude-code/skills/webapp-testing/utils/__pycache__/form_helpers.cpython-312.pyc
new file mode 100644
index 0000000..93f6999
Binary files /dev/null and b/claude-code/skills/webapp-testing/utils/__pycache__/form_helpers.cpython-312.pyc differ
diff --git a/claude-code/skills/webapp-testing/utils/__pycache__/supabase.cpython-312.pyc b/claude-code/skills/webapp-testing/utils/__pycache__/supabase.cpython-312.pyc
new file mode 100644
index 0000000..989c929
Binary files /dev/null and b/claude-code/skills/webapp-testing/utils/__pycache__/supabase.cpython-312.pyc differ
diff --git a/claude-code/skills/webapp-testing/utils/__pycache__/ui_interactions.cpython-312.pyc b/claude-code/skills/webapp-testing/utils/__pycache__/ui_interactions.cpython-312.pyc
new file mode 100644
index 0000000..0b547b4
Binary files /dev/null and b/claude-code/skills/webapp-testing/utils/__pycache__/ui_interactions.cpython-312.pyc differ
diff --git a/claude-code/skills/webapp-testing/utils/__pycache__/wait_strategies.cpython-312.pyc b/claude-code/skills/webapp-testing/utils/__pycache__/wait_strategies.cpython-312.pyc
new file mode 100644
index 0000000..2de673e
Binary files /dev/null and b/claude-code/skills/webapp-testing/utils/__pycache__/wait_strategies.cpython-312.pyc differ
diff --git a/claude-code/skills/webapp-testing/utils/form_helpers.py b/claude-code/skills/webapp-testing/utils/form_helpers.py
new file mode 100644
index 0000000..e011f5f
--- /dev/null
+++ b/claude-code/skills/webapp-testing/utils/form_helpers.py
@@ -0,0 +1,463 @@
+"""
+Smart Form Filling Helpers
+
+Handles common form patterns across web applications:
+- Multi-step forms with validation
+- Dynamic field variations (full name vs first/last name)
+- Retry strategies for flaky selectors
+- Intelligent field detection
+"""
+
+from playwright.sync_api import Page
+from typing import Dict, List, Any, Optional
+import time
+
+
+class SmartFormFiller:
+ """
+ Intelligent form filling that handles variations in field structures.
+
+ Example:
+ ```python
+ filler = SmartFormFiller()
+ filler.fill_name_field(page, "John Doe") # Tries full name or first/last
+ filler.fill_email_field(page, "test@example.com")
+ filler.fill_password_fields(page, "SecurePass123!")
+ ```
+ """
+
+ @staticmethod
+ def fill_name_field(page: Page, full_name: str, timeout: int = 5000) -> bool:
+ """
+ Fill name field(s) - handles both single "Full Name" and separate "First/Last Name" fields.
+
+ Args:
+ page: Playwright Page object
+ full_name: Full name as string (e.g., "John Doe")
+ timeout: Maximum time to wait for fields (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ # Works with both field structures:
+ # - Single field: "Full Name"
+ # - Separate fields: "First Name" and "Last Name"
+ fill_name_field(page, "Jane Smith")
+ ```
+ """
+ # Strategy 1: Try single "Full Name" field
+ full_name_selectors = [
+ 'input[name*="full" i][name*="name" i]',
+ 'input[placeholder*="full name" i]',
+ 'input[placeholder*="name" i]',
+ 'input[id*="fullname" i]',
+ 'input[id*="full-name" i]',
+ ]
+
+ for selector in full_name_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(full_name)
+ return True
+ except:
+ continue
+
+ # Strategy 2: Try separate First/Last Name fields
+ parts = full_name.split(' ', 1)
+ first_name = parts[0] if parts else full_name
+ last_name = parts[1] if len(parts) > 1 else ''
+
+ first_name_selectors = [
+ 'input[name*="first" i][name*="name" i]',
+ 'input[placeholder*="first name" i]',
+ 'input[id*="firstname" i]',
+ 'input[id*="first-name" i]',
+ ]
+
+ last_name_selectors = [
+ 'input[name*="last" i][name*="name" i]',
+ 'input[placeholder*="last name" i]',
+ 'input[id*="lastname" i]',
+ 'input[id*="last-name" i]',
+ ]
+
+ first_filled = False
+ last_filled = False
+
+ # Fill first name
+ for selector in first_name_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(first_name)
+ first_filled = True
+ break
+ except:
+ continue
+
+ # Fill last name
+ for selector in last_name_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(last_name)
+ last_filled = True
+ break
+ except:
+ continue
+
+ return first_filled or last_filled
+
+ @staticmethod
+ def fill_email_field(page: Page, email: str, timeout: int = 5000) -> bool:
+ """
+ Fill email field with multiple selector strategies.
+
+ Args:
+ page: Playwright Page object
+ email: Email address
+ timeout: Maximum time to wait for field (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ email_selectors = [
+ 'input[type="email"]',
+ 'input[name="email" i]',
+ 'input[placeholder*="email" i]',
+ 'input[id*="email" i]',
+ 'input[autocomplete="email"]',
+ ]
+
+ for selector in email_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(email)
+ return True
+ except:
+ continue
+
+ return False
+
+ @staticmethod
+ def fill_password_fields(page: Page, password: str, confirm: bool = True, timeout: int = 5000) -> bool:
+ """
+ Fill password field(s) - handles both single password and password + confirm.
+
+ Args:
+ page: Playwright Page object
+ password: Password string
+ confirm: Whether to also fill confirmation field (default True)
+ timeout: Maximum time to wait for fields (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ password_fields = page.locator('input[type="password"]').all()
+
+ if not password_fields:
+ return False
+
+ # Fill first password field
+ try:
+ password_fields[0].fill(password)
+ except:
+ return False
+
+ # Fill confirmation field if requested and exists
+ if confirm and len(password_fields) > 1:
+ try:
+ password_fields[1].fill(password)
+ except:
+ pass
+
+ return True
+
+ @staticmethod
+ def fill_phone_field(page: Page, phone: str, timeout: int = 5000) -> bool:
+ """
+ Fill phone number field with multiple selector strategies.
+
+ Args:
+ page: Playwright Page object
+ phone: Phone number string
+ timeout: Maximum time to wait for field (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+ """
+ phone_selectors = [
+ 'input[type="tel"]',
+ 'input[name*="phone" i]',
+ 'input[placeholder*="phone" i]',
+ 'input[id*="phone" i]',
+ 'input[autocomplete="tel"]',
+ ]
+
+ for selector in phone_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(phone)
+ return True
+ except:
+ continue
+
+ return False
+
+ @staticmethod
+ def fill_date_field(page: Page, date_value: str, field_hint: str = None, timeout: int = 5000) -> bool:
+ """
+ Fill date field (handles both date input and text input).
+
+ Args:
+ page: Playwright Page object
+ date_value: Date as string (format: YYYY-MM-DD for date inputs)
+ field_hint: Optional hint about field (e.g., "birth", "start", "end")
+ timeout: Maximum time to wait for field (milliseconds)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ fill_date_field(page, "1990-01-15", field_hint="birth")
+ ```
+ """
+ # Build selectors based on hint
+ date_selectors = ['input[type="date"]']
+
+ if field_hint:
+ date_selectors.extend([
+ f'input[name*="{field_hint}" i]',
+ f'input[placeholder*="{field_hint}" i]',
+ f'input[id*="{field_hint}" i]',
+ ])
+
+ for selector in date_selectors:
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(date_value)
+ return True
+ except:
+ continue
+
+ return False
+
+
+def fill_with_retry(page: Page, selectors: List[str], value: str, max_attempts: int = 3) -> bool:
+ """
+ Try multiple selectors with retry logic.
+
+ Args:
+ page: Playwright Page object
+ selectors: List of CSS selectors to try
+ value: Value to fill
+ max_attempts: Maximum retry attempts per selector
+
+ Returns:
+ True if any selector succeeded, False otherwise
+
+ Example:
+ ```python
+ selectors = ['input#email', 'input[name="email"]', 'input[type="email"]']
+ fill_with_retry(page, selectors, 'test@example.com')
+ ```
+ """
+ for selector in selectors:
+ for attempt in range(max_attempts):
+ try:
+ field = page.locator(selector).first
+ if field.is_visible(timeout=1000):
+ field.fill(value)
+ time.sleep(0.3)
+ # Verify value was set
+ if field.input_value() == value:
+ return True
+ except:
+ if attempt < max_attempts - 1:
+ time.sleep(0.5)
+ continue
+
+ return False
+
+
+def handle_multi_step_form(page: Page, steps: List[Dict[str, Any]], continue_button_text: str = "CONTINUE") -> bool:
+ """
+ Automate multi-step form completion.
+
+ Args:
+ page: Playwright Page object
+ steps: List of step configurations, each with fields and actions
+ continue_button_text: Text of button to advance steps
+
+ Returns:
+ True if all steps completed successfully, False otherwise
+
+ Example:
+ ```python
+ steps = [
+ {
+ 'fields': {'email': 'test@example.com', 'password': 'Pass123!'},
+ 'checkbox': 'terms', # Optional checkbox to check
+ 'wait_after': 2, # Optional wait time after step
+ },
+ {
+ 'fields': {'full_name': 'John Doe', 'date_of_birth': '1990-01-15'},
+ },
+ {
+ 'complete': True, # Final step, click complete/finish button
+ }
+ ]
+ handle_multi_step_form(page, steps)
+ ```
+ """
+ filler = SmartFormFiller()
+
+ for i, step in enumerate(steps):
+ print(f" Processing step {i+1}/{len(steps)}...")
+
+ # Fill fields in this step
+ if 'fields' in step:
+ for field_type, value in step['fields'].items():
+ if field_type == 'email':
+ filler.fill_email_field(page, value)
+ elif field_type == 'password':
+ filler.fill_password_fields(page, value)
+ elif field_type == 'full_name':
+ filler.fill_name_field(page, value)
+ elif field_type == 'phone':
+ filler.fill_phone_field(page, value)
+ elif field_type.startswith('date'):
+ hint = field_type.replace('date_', '').replace('_', ' ')
+ filler.fill_date_field(page, value, field_hint=hint)
+ else:
+ # Generic field - try to find and fill
+ print(f" Warning: Unknown field type '{field_type}'")
+
+ # Check checkbox if specified
+ if 'checkbox' in step:
+ try:
+ checkbox = page.locator('input[type="checkbox"]').first
+ checkbox.check()
+ except:
+ print(f" Warning: Could not check checkbox")
+
+ # Wait if specified
+ if 'wait_after' in step:
+ time.sleep(step['wait_after'])
+ else:
+ time.sleep(1)
+
+ # Click continue/submit button
+ if i < len(steps) - 1: # Not the last step
+ button_selectors = [
+ f'button:has-text("{continue_button_text}")',
+ 'button[type="submit"]',
+ 'button:has-text("Next")',
+ 'button:has-text("Continue")',
+ ]
+
+ clicked = False
+ for selector in button_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=2000):
+ button.click()
+ clicked = True
+ break
+ except:
+ continue
+
+ if not clicked:
+ print(f" Warning: Could not find continue button for step {i+1}")
+ return False
+
+ # Wait for next step to load
+ page.wait_for_load_state('networkidle')
+ time.sleep(2)
+
+ else: # Last step
+ if step.get('complete', False):
+ complete_selectors = [
+ 'button:has-text("COMPLETE")',
+ 'button:has-text("Complete")',
+ 'button:has-text("FINISH")',
+ 'button:has-text("Finish")',
+ 'button:has-text("SUBMIT")',
+ 'button:has-text("Submit")',
+ 'button[type="submit"]',
+ ]
+
+ for selector in complete_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=2000):
+ button.click()
+ page.wait_for_load_state('networkidle')
+ time.sleep(3)
+ return True
+ except:
+ continue
+
+ print(" Warning: Could not find completion button")
+ return False
+
+ return True
+
+
+def auto_fill_form(page: Page, field_mapping: Dict[str, str]) -> Dict[str, bool]:
+ """
+ Automatically fill a form based on field mapping.
+
+ Intelligently detects field types and uses appropriate filling strategies.
+
+ Args:
+ page: Playwright Page object
+ field_mapping: Dictionary mapping field types to values
+
+ Returns:
+ Dictionary with results for each field (True = filled, False = failed)
+
+ Example:
+ ```python
+ results = auto_fill_form(page, {
+ 'email': 'test@example.com',
+ 'password': 'SecurePass123!',
+ 'full_name': 'Jane Doe',
+ 'phone': '+447700900123',
+ 'date_of_birth': '1990-01-15',
+ })
+ print(f"Email filled: {results['email']}")
+ ```
+ """
+ filler = SmartFormFiller()
+ results = {}
+
+ for field_type, value in field_mapping.items():
+ if field_type == 'email':
+ results[field_type] = filler.fill_email_field(page, value)
+ elif field_type == 'password':
+ results[field_type] = filler.fill_password_fields(page, value)
+ elif 'name' in field_type.lower():
+ results[field_type] = filler.fill_name_field(page, value)
+ elif 'phone' in field_type.lower():
+ results[field_type] = filler.fill_phone_field(page, value)
+ elif 'date' in field_type.lower():
+ hint = field_type.replace('date_of_', '').replace('_', ' ')
+ results[field_type] = filler.fill_date_field(page, value, field_hint=hint)
+ else:
+ # Try generic fill
+ try:
+ field = page.locator(f'input[name="{field_type}"]').first
+ field.fill(value)
+ results[field_type] = True
+ except:
+ results[field_type] = False
+
+ return results
diff --git a/claude-code/skills/webapp-testing/utils/supabase.py b/claude-code/skills/webapp-testing/utils/supabase.py
new file mode 100644
index 0000000..ecceac2
--- /dev/null
+++ b/claude-code/skills/webapp-testing/utils/supabase.py
@@ -0,0 +1,353 @@
+"""
+Supabase Test Utilities
+
+Generic database helpers for testing with Supabase.
+Supports user management, email verification, and test data cleanup.
+"""
+
+import subprocess
+import json
+from typing import Dict, List, Optional, Any
+
+
+class SupabaseTestClient:
+ """
+ Generic Supabase test client for database operations during testing.
+
+ Example:
+ ```python
+ client = SupabaseTestClient(
+ url="https://project.supabase.co",
+ service_key="your-service-role-key",
+ db_password="your-db-password"
+ )
+
+ # Create test user
+ user_id = client.create_user("test@example.com", "password123")
+
+ # Verify email (bypass email sending)
+ client.confirm_email(user_id)
+
+ # Cleanup after test
+ client.delete_user(user_id)
+ ```
+ """
+
+ def __init__(self, url: str, service_key: str, db_password: str = None, db_host: str = None):
+ """
+ Initialize Supabase test client.
+
+ Args:
+ url: Supabase project URL (e.g., "https://project.supabase.co")
+ service_key: Service role key for admin operations
+ db_password: Database password for direct SQL operations
+ db_host: Database host (if different from default)
+ """
+ self.url = url.rstrip('/')
+ self.service_key = service_key
+ self.db_password = db_password
+
+ # Extract DB host from URL if not provided
+ if not db_host:
+ # Convert https://abc123.supabase.co to db.abc123.supabase.co
+ project_ref = url.split('//')[1].split('.')[0]
+ self.db_host = f"db.{project_ref}.supabase.co"
+ else:
+ self.db_host = db_host
+
+ def _run_sql(self, sql: str) -> Dict[str, Any]:
+ """
+ Execute SQL directly against the database.
+
+ Args:
+ sql: SQL query to execute
+
+ Returns:
+ Dictionary with 'success', 'output', 'error' keys
+ """
+ if not self.db_password:
+ return {'success': False, 'error': 'Database password not provided'}
+
+ try:
+ result = subprocess.run(
+ [
+ 'psql',
+ '-h', self.db_host,
+ '-p', '5432',
+ '-U', 'postgres',
+ '-c', sql,
+ '-t', # Tuples only
+ '-A', # Unaligned output
+ ],
+ env={'PGPASSWORD': self.db_password},
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+
+ return {
+ 'success': result.returncode == 0,
+ 'output': result.stdout.strip(),
+ 'error': result.stderr.strip() if result.returncode != 0 else None
+ }
+ except Exception as e:
+ return {'success': False, 'error': str(e)}
+
+ def create_user(self, email: str, password: str, metadata: Dict = None) -> Optional[str]:
+ """
+ Create a test user via Auth Admin API.
+
+ Args:
+ email: User email
+ password: User password
+ metadata: Optional user metadata
+
+ Returns:
+ User ID if successful, None otherwise
+
+ Example:
+ ```python
+ user_id = client.create_user(
+ "test@example.com",
+ "SecurePass123!",
+ metadata={"full_name": "Test User"}
+ )
+ ```
+ """
+ import requests
+
+ payload = {
+ 'email': email,
+ 'password': password,
+ 'email_confirm': True
+ }
+
+ if metadata:
+ payload['user_metadata'] = metadata
+
+ try:
+ response = requests.post(
+ f"{self.url}/auth/v1/admin/users",
+ headers={
+ 'Authorization': f'Bearer {self.service_key}',
+ 'apikey': self.service_key,
+ 'Content-Type': 'application/json'
+ },
+ json=payload,
+ timeout=10
+ )
+
+ if response.ok:
+ return response.json().get('id')
+ else:
+ print(f"Error creating user: {response.text}")
+ return None
+ except Exception as e:
+ print(f"Exception creating user: {e}")
+ return None
+
+ def confirm_email(self, user_id: str = None, email: str = None) -> bool:
+ """
+ Confirm user email (bypass email verification for testing).
+
+ Args:
+ user_id: User ID (if known)
+ email: User email (alternative to user_id)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ # By user ID
+ client.confirm_email(user_id="abc-123")
+
+ # Or by email
+ client.confirm_email(email="test@example.com")
+ ```
+ """
+ if user_id:
+ sql = f"UPDATE auth.users SET email_confirmed_at = NOW() WHERE id = '{user_id}';"
+ elif email:
+ sql = f"UPDATE auth.users SET email_confirmed_at = NOW() WHERE email = '{email}';"
+ else:
+ return False
+
+ result = self._run_sql(sql)
+ return result['success']
+
+ def delete_user(self, user_id: str = None, email: str = None) -> bool:
+ """
+ Delete a test user and related data.
+
+ Args:
+ user_id: User ID
+ email: User email (alternative to user_id)
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ client.delete_user(email="test@example.com")
+ ```
+ """
+ # Get user ID if email provided
+ if email and not user_id:
+ result = self._run_sql(f"SELECT id FROM auth.users WHERE email = '{email}';")
+ if result['success'] and result['output']:
+ user_id = result['output'].strip()
+ else:
+ return False
+
+ if not user_id:
+ return False
+
+ # Delete from profiles first (foreign key)
+ self._run_sql(f"DELETE FROM public.profiles WHERE id = '{user_id}';")
+
+ # Delete from auth.users
+ result = self._run_sql(f"DELETE FROM auth.users WHERE id = '{user_id}';")
+
+ return result['success']
+
+ def cleanup_related_records(self, user_id: str, tables: List[str] = None) -> Dict[str, bool]:
+ """
+ Clean up user-related records from multiple tables.
+
+ Args:
+ user_id: User ID
+ tables: List of tables to clean (defaults to common tables)
+
+ Returns:
+ Dictionary mapping table names to cleanup success status
+
+ Example:
+ ```python
+ results = client.cleanup_related_records(
+ user_id="abc-123",
+ tables=["profiles", "team_members", "coach_verification_requests"]
+ )
+ ```
+ """
+ if not tables:
+ tables = [
+ 'pending_profiles',
+ 'coach_verification_requests',
+ 'team_members',
+ 'team_join_requests',
+ 'profiles'
+ ]
+
+ results = {}
+
+ for table in tables:
+ # Try both user_id and id columns
+ sql = f"DELETE FROM public.{table} WHERE user_id = '{user_id}' OR id = '{user_id}';"
+ result = self._run_sql(sql)
+ results[table] = result['success']
+
+ return results
+
+ def create_invite_code(self, code: str, code_type: str = 'general', max_uses: int = 999) -> bool:
+ """
+ Create an invite code for testing.
+
+ Args:
+ code: Invite code string
+ code_type: Type of code (e.g., 'general', 'team_join')
+ max_uses: Maximum number of uses
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ client.create_invite_code("TEST2024", code_type="general")
+ ```
+ """
+ sql = f"""
+ INSERT INTO public.invite_codes (code, code_type, is_valid, max_uses, expires_at)
+ VALUES ('{code}', '{code_type}', true, {max_uses}, NOW() + INTERVAL '30 days')
+ ON CONFLICT (code) DO UPDATE SET is_valid=true, max_uses={max_uses}, use_count=0;
+ """
+
+ result = self._run_sql(sql)
+ return result['success']
+
+ def find_user_by_email(self, email: str) -> Optional[str]:
+ """
+ Find user ID by email address.
+
+ Args:
+ email: User email
+
+ Returns:
+ User ID if found, None otherwise
+ """
+ sql = f"SELECT id FROM auth.users WHERE email = '{email}';"
+ result = self._run_sql(sql)
+
+ if result['success'] and result['output']:
+ return result['output'].strip()
+ return None
+
+ def get_user_privileges(self, user_id: str) -> Optional[List[str]]:
+ """
+ Get user's privilege array.
+
+ Args:
+ user_id: User ID
+
+ Returns:
+ List of privileges if found, None otherwise
+ """
+ sql = f"SELECT privileges FROM public.profiles WHERE id = '{user_id}';"
+ result = self._run_sql(sql)
+
+ if result['success'] and result['output']:
+ # Parse PostgreSQL array format
+ privileges_str = result['output'].strip('{}')
+ return [p.strip() for p in privileges_str.split(',')]
+ return None
+
+
+def quick_cleanup(email: str, db_password: str, project_url: str) -> bool:
+ """
+ Quick cleanup helper - delete user and all related data.
+
+ Args:
+ email: User email to delete
+ db_password: Database password
+ project_url: Supabase project URL
+
+ Returns:
+ True if successful, False otherwise
+
+ Example:
+ ```python
+ from utils.supabase import quick_cleanup
+
+ # Clean up test user
+ quick_cleanup(
+ "test@example.com",
+ "db_password",
+ "https://project.supabase.co"
+ )
+ ```
+ """
+ client = SupabaseTestClient(
+ url=project_url,
+ service_key="", # Not needed for SQL operations
+ db_password=db_password
+ )
+
+ user_id = client.find_user_by_email(email)
+ if not user_id:
+ return True # Already deleted
+
+ # Clean up all related tables
+ client.cleanup_related_records(user_id)
+
+ # Delete user
+ return client.delete_user(user_id)
diff --git a/claude-code/skills/webapp-testing/utils/ui_interactions.py b/claude-code/skills/webapp-testing/utils/ui_interactions.py
new file mode 100644
index 0000000..1066edf
--- /dev/null
+++ b/claude-code/skills/webapp-testing/utils/ui_interactions.py
@@ -0,0 +1,382 @@
+"""
+UI Interaction Helpers for Web Automation
+
+Common UI patterns that appear across many web applications:
+- Cookie consent banners
+- Modal dialogs
+- Loading overlays
+- Welcome tours/onboarding
+- Fixed headers blocking clicks
+"""
+
+from playwright.sync_api import Page
+import time
+
+
+def dismiss_cookie_banner(page: Page, timeout: int = 3000) -> bool:
+ """
+ Detect and dismiss cookie consent banners.
+
+ Tries common patterns:
+ - "Accept" / "Accept All" / "OK" buttons
+ - "I Agree" / "Got it" buttons
+ - Cookie banner containers
+
+ Args:
+ page: Playwright Page object
+ timeout: Maximum time to wait for banner (milliseconds)
+
+ Returns:
+ True if banner was found and dismissed, False otherwise
+
+ Example:
+ ```python
+ page.goto('https://example.com')
+ if dismiss_cookie_banner(page):
+ print("Cookie banner dismissed")
+ ```
+ """
+ cookie_button_selectors = [
+ 'button:has-text("Accept")',
+ 'button:has-text("Accept All")',
+ 'button:has-text("Accept all")',
+ 'button:has-text("I Agree")',
+ 'button:has-text("I agree")',
+ 'button:has-text("OK")',
+ 'button:has-text("Got it")',
+ 'button:has-text("Allow")',
+ 'button:has-text("Allow all")',
+ '[data-testid="cookie-accept"]',
+ '[data-testid="accept-cookies"]',
+ '[id*="cookie-accept" i]',
+ '[id*="accept-cookie" i]',
+ '[class*="cookie-accept" i]',
+ ]
+
+ for selector in cookie_button_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=timeout):
+ button.click()
+ time.sleep(0.5) # Brief wait for banner to disappear
+ return True
+ except:
+ continue
+
+ return False
+
+
+def dismiss_modal(page: Page, modal_identifier: str = None, timeout: int = 2000) -> bool:
+ """
+ Close modal dialogs with multiple fallback strategies.
+
+ Strategies:
+ 1. If identifier provided, close that specific modal
+ 2. Click close button (X, Close, Cancel, etc.)
+ 3. Press Escape key
+ 4. Click backdrop/overlay
+
+ Args:
+ page: Playwright Page object
+ modal_identifier: Optional - specific text in modal to identify it
+ timeout: Maximum time to wait for modal (milliseconds)
+
+ Returns:
+ True if modal was found and closed, False otherwise
+
+ Example:
+ ```python
+ # Close any modal
+ dismiss_modal(page)
+
+ # Close specific "Welcome" modal
+ dismiss_modal(page, modal_identifier="Welcome")
+ ```
+ """
+ # If specific modal identifier provided, wait for it first
+ if modal_identifier:
+ try:
+ modal = page.locator(f'[role="dialog"]:has-text("{modal_identifier}"), dialog:has-text("{modal_identifier}")').first
+ if not modal.is_visible(timeout=timeout):
+ return False
+ except:
+ return False
+
+ # Strategy 1: Click close button
+ close_button_selectors = [
+ 'button:has-text("Close")',
+ 'button:has-text("×")',
+ 'button:has-text("X")',
+ 'button:has-text("Cancel")',
+ 'button:has-text("GOT IT")',
+ 'button:has-text("Got it")',
+ 'button:has-text("OK")',
+ 'button:has-text("Dismiss")',
+ '[aria-label="Close"]',
+ '[aria-label="close"]',
+ '[data-testid="close-modal"]',
+ '[class*="close" i]',
+ '[class*="dismiss" i]',
+ ]
+
+ for selector in close_button_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=500):
+ button.click()
+ time.sleep(0.5)
+ return True
+ except:
+ continue
+
+ # Strategy 2: Press Escape key
+ try:
+ page.keyboard.press('Escape')
+ time.sleep(0.5)
+ # Check if modal is gone
+ modals = page.locator('[role="dialog"], dialog').all()
+ if all(not m.is_visible() for m in modals):
+ return True
+ except:
+ pass
+
+ # Strategy 3: Click backdrop (if exists and clickable)
+ try:
+ backdrop = page.locator('[class*="backdrop" i], [class*="overlay" i]').first
+ if backdrop.is_visible(timeout=500):
+ backdrop.click(position={'x': 10, 'y': 10}) # Click corner, not center
+ time.sleep(0.5)
+ return True
+ except:
+ pass
+
+ return False
+
+
+def click_with_header_offset(page: Page, selector: str, header_height: int = 80, force: bool = False):
+ """
+ Click an element while accounting for fixed headers that might block it.
+
+ Scrolls the element into view with an offset to avoid fixed headers,
+ then clicks it.
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element to click
+ header_height: Height of fixed header in pixels (default 80)
+ force: Whether to use force click if normal click fails
+
+ Example:
+ ```python
+ # Click button that might be behind a fixed header
+ click_with_header_offset(page, 'button#submit', header_height=100)
+ ```
+ """
+ element = page.locator(selector).first
+
+ # Scroll element into view with offset
+ element.evaluate(f'el => el.scrollIntoView({{ block: "center", inline: "nearest" }})')
+ page.evaluate(f'window.scrollBy(0, -{header_height})')
+ time.sleep(0.3) # Brief wait for scroll to complete
+
+ try:
+ element.click()
+ except Exception as e:
+ if force:
+ element.click(force=True)
+ else:
+ raise e
+
+
+def force_click_if_needed(page: Page, selector: str, timeout: int = 5000) -> bool:
+ """
+ Try normal click first, use force click if it fails (e.g., due to overlays).
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element to click
+ timeout: Maximum time to wait for element (milliseconds)
+
+ Returns:
+ True if click succeeded (normal or forced), False otherwise
+
+ Example:
+ ```python
+ # Try to click, handling potential overlays
+ if force_click_if_needed(page, 'button#submit'):
+ print("Button clicked successfully")
+ ```
+ """
+ try:
+ element = page.locator(selector).first
+ if not element.is_visible(timeout=timeout):
+ return False
+
+ # Try normal click first
+ try:
+ element.click(timeout=timeout)
+ return True
+ except:
+ # Fall back to force click
+ element.click(force=True)
+ return True
+ except:
+ return False
+
+
+def wait_for_no_overlay(page: Page, max_wait_seconds: int = 10) -> bool:
+ """
+ Wait for loading overlays/spinners to disappear.
+
+ Looks for common loading overlay patterns and waits until they're gone.
+
+ Args:
+ page: Playwright Page object
+ max_wait_seconds: Maximum time to wait (seconds)
+
+ Returns:
+ True if overlays disappeared, False if timeout
+
+ Example:
+ ```python
+ page.click('button#submit')
+ wait_for_no_overlay(page) # Wait for loading to complete
+ ```
+ """
+ overlay_selectors = [
+ '[class*="loading" i]',
+ '[class*="spinner" i]',
+ '[class*="overlay" i]',
+ '[class*="backdrop" i]',
+ '[data-loading="true"]',
+ '[aria-busy="true"]',
+ '.loader',
+ '.loading',
+ '#loading',
+ ]
+
+ start_time = time.time()
+
+ while time.time() - start_time < max_wait_seconds:
+ all_hidden = True
+
+ for selector in overlay_selectors:
+ try:
+ overlays = page.locator(selector).all()
+ for overlay in overlays:
+ if overlay.is_visible():
+ all_hidden = False
+ break
+ except:
+ continue
+
+ if not all_hidden:
+ break
+
+ if all_hidden:
+ return True
+
+ time.sleep(0.5)
+
+ return False
+
+
+def handle_welcome_tour(page: Page, skip_button_text: str = "Skip") -> bool:
+ """
+ Automatically skip onboarding tours or welcome wizards.
+
+ Looks for and clicks "Skip", "Skip Tour", "Close", "Maybe Later" buttons.
+
+ Args:
+ page: Playwright Page object
+ skip_button_text: Text to look for in skip buttons (default "Skip")
+
+ Returns:
+ True if tour was skipped, False if no tour found
+
+ Example:
+ ```python
+ page.goto('https://app.example.com')
+ handle_welcome_tour(page) # Skip any onboarding tour
+ ```
+ """
+ skip_selectors = [
+ f'button:has-text("{skip_button_text}")',
+ 'button:has-text("Skip Tour")',
+ 'button:has-text("Maybe Later")',
+ 'button:has-text("No Thanks")',
+ 'button:has-text("Close Tour")',
+ '[data-testid="skip-tour"]',
+ '[data-testid="close-tour"]',
+ '[aria-label="Skip tour"]',
+ '[aria-label="Close tour"]',
+ ]
+
+ for selector in skip_selectors:
+ try:
+ button = page.locator(selector).first
+ if button.is_visible(timeout=2000):
+ button.click()
+ time.sleep(0.5)
+ return True
+ except:
+ continue
+
+ return False
+
+
+def wait_for_stable_dom(page: Page, stability_duration_ms: int = 1000, max_wait_seconds: int = 10) -> bool:
+ """
+ Wait for the DOM to stop changing (useful for dynamic content loading).
+
+ Monitors for DOM mutations and waits until no changes occur for the specified duration.
+
+ Args:
+ page: Playwright Page object
+ stability_duration_ms: Duration of no changes to consider stable (milliseconds)
+ max_wait_seconds: Maximum time to wait (seconds)
+
+ Returns:
+ True if DOM stabilized, False if timeout
+
+ Example:
+ ```python
+ page.goto('https://app.example.com')
+ wait_for_stable_dom(page) # Wait for all dynamic content to load
+ ```
+ """
+ # Inject mutation observer script
+ script = f"""
+ new Promise((resolve) => {{
+ let lastMutation = Date.now();
+ const observer = new MutationObserver(() => {{
+ lastMutation = Date.now();
+ }});
+
+ observer.observe(document.body, {{
+ childList: true,
+ subtree: true,
+ attributes: true
+ }});
+
+ const checkStability = () => {{
+ if (Date.now() - lastMutation >= {stability_duration_ms}) {{
+ observer.disconnect();
+ resolve(true);
+ }} else if (Date.now() - lastMutation > {max_wait_seconds * 1000}) {{
+ observer.disconnect();
+ resolve(false);
+ }} else {{
+ setTimeout(checkStability, 100);
+ }}
+ }};
+
+ setTimeout(checkStability, {stability_duration_ms});
+ }})
+ """
+
+ try:
+ result = page.evaluate(script)
+ return result
+ except:
+ return False
diff --git a/claude-code/skills/webapp-testing/utils/wait_strategies.py b/claude-code/skills/webapp-testing/utils/wait_strategies.py
new file mode 100644
index 0000000..f92d236
--- /dev/null
+++ b/claude-code/skills/webapp-testing/utils/wait_strategies.py
@@ -0,0 +1,312 @@
+"""
+Advanced Wait Strategies for Reliable Web Automation
+
+Better alternatives to simple sleep() or networkidle for dynamic web applications.
+"""
+
+from playwright.sync_api import Page
+import time
+from typing import Callable, Optional, Any
+
+
+def wait_for_api_call(page: Page, url_pattern: str, timeout_seconds: int = 10) -> Optional[Any]:
+ """
+ Wait for a specific API call to complete and return its response.
+
+ Args:
+ page: Playwright Page object
+ url_pattern: URL pattern to match (can include wildcards)
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ Response data if call completed, None if timeout
+
+ Example:
+ ```python
+ # Wait for user profile API call
+ response = wait_for_api_call(page, '**/api/profile**')
+ if response:
+ print(f"Profile loaded: {response}")
+ ```
+ """
+ response_data = {'data': None, 'completed': False}
+
+ def handle_response(response):
+ if url_pattern.replace('**', '') in response.url:
+ try:
+ response_data['data'] = response.json()
+ response_data['completed'] = True
+ except:
+ response_data['completed'] = True
+
+ page.on('response', handle_response)
+
+ start_time = time.time()
+ while not response_data['completed'] and (time.time() - start_time) < timeout_seconds:
+ time.sleep(0.1)
+
+ page.remove_listener('response', handle_response)
+
+ return response_data['data']
+
+
+def wait_for_element_stable(page: Page, selector: str, stability_ms: int = 1000, timeout_seconds: int = 10) -> bool:
+ """
+ Wait for an element's position to stabilize (stop moving/changing).
+
+ Useful for elements that animate or shift due to dynamic content loading.
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element
+ stability_ms: Duration element must remain stable (milliseconds)
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if element stabilized, False if timeout
+
+ Example:
+ ```python
+ # Wait for dropdown menu to finish animating
+ wait_for_element_stable(page, '.dropdown-menu', stability_ms=500)
+ ```
+ """
+ try:
+ element = page.locator(selector).first
+
+ script = f"""
+ (element, stabilityMs) => {{
+ return new Promise((resolve) => {{
+ let lastRect = element.getBoundingClientRect();
+ let lastChange = Date.now();
+
+ const checkStability = () => {{
+ const currentRect = element.getBoundingClientRect();
+
+ if (currentRect.top !== lastRect.top ||
+ currentRect.left !== lastRect.left ||
+ currentRect.width !== lastRect.width ||
+ currentRect.height !== lastRect.height) {{
+ lastChange = Date.now();
+ lastRect = currentRect;
+ }}
+
+ if (Date.now() - lastChange >= stabilityMs) {{
+ resolve(true);
+ }} else if (Date.now() - lastChange < {timeout_seconds * 1000}) {{
+ setTimeout(checkStability, 50);
+ }} else {{
+ resolve(false);
+ }}
+ }};
+
+ setTimeout(checkStability, stabilityMs);
+ }});
+ }}
+ """
+
+ result = element.evaluate(script, stability_ms)
+ return result
+ except:
+ return False
+
+
+def wait_with_retry(page: Page, condition_fn: Callable[[], bool], max_retries: int = 5, backoff_seconds: float = 0.5) -> bool:
+ """
+ Wait for a condition with exponential backoff retry.
+
+ Args:
+ page: Playwright Page object
+ condition_fn: Function that returns True when condition is met
+ max_retries: Maximum number of retry attempts
+ backoff_seconds: Initial backoff duration (doubles each retry)
+
+ Returns:
+ True if condition met, False if all retries exhausted
+
+ Example:
+ ```python
+ # Wait for specific element to appear with retry
+ def check_dashboard():
+ return page.locator('#dashboard').is_visible()
+
+ if wait_with_retry(page, check_dashboard):
+ print("Dashboard loaded!")
+ ```
+ """
+ wait_time = backoff_seconds
+
+ for attempt in range(max_retries):
+ try:
+ if condition_fn():
+ return True
+ except:
+ pass
+
+ if attempt < max_retries - 1:
+ time.sleep(wait_time)
+ wait_time *= 2 # Exponential backoff
+
+ return False
+
+
+def smart_navigation_wait(page: Page, expected_url_pattern: str = None, timeout_seconds: int = 10) -> bool:
+ """
+ Comprehensive wait strategy after navigation/interaction.
+
+ Combines multiple strategies:
+ 1. Network idle
+ 2. DOM stability
+ 3. URL pattern match (if provided)
+
+ Args:
+ page: Playwright Page object
+ expected_url_pattern: Optional URL pattern to wait for
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if all conditions met, False if timeout
+
+ Example:
+ ```python
+ page.click('button#login')
+ smart_navigation_wait(page, expected_url_pattern='**/dashboard**')
+ ```
+ """
+ start_time = time.time()
+
+ # Step 1: Wait for network idle
+ try:
+ page.wait_for_load_state('networkidle', timeout=timeout_seconds * 1000)
+ except:
+ pass
+
+ # Step 2: Check URL if pattern provided
+ if expected_url_pattern:
+ while (time.time() - start_time) < timeout_seconds:
+ current_url = page.url
+ pattern = expected_url_pattern.replace('**', '')
+ if pattern in current_url:
+ break
+ time.sleep(0.5)
+ else:
+ return False
+
+ # Step 3: Brief wait for DOM stability
+ time.sleep(1)
+
+ return True
+
+
+def wait_for_data_load(page: Page, data_attribute: str = 'data-loaded', timeout_seconds: int = 10) -> bool:
+ """
+ Wait for data-loading attribute to indicate completion.
+
+ Args:
+ page: Playwright Page object
+ data_attribute: Data attribute to check (e.g., 'data-loaded')
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if data loaded, False if timeout
+
+ Example:
+ ```python
+ # Wait for element with data-loaded="true"
+ wait_for_data_load(page, data_attribute='data-loaded')
+ ```
+ """
+ start_time = time.time()
+
+ while (time.time() - start_time) < timeout_seconds:
+ try:
+ elements = page.locator(f'[{data_attribute}="true"]').all()
+ if elements:
+ return True
+ except:
+ pass
+
+ time.sleep(0.3)
+
+ return False
+
+
+def wait_until_no_element(page: Page, selector: str, timeout_seconds: int = 10) -> bool:
+ """
+ Wait until an element is no longer visible (e.g., loading spinner disappears).
+
+ Args:
+ page: Playwright Page object
+ selector: CSS selector for the element
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if element disappeared, False if still visible after timeout
+
+ Example:
+ ```python
+ # Wait for loading spinner to disappear
+ wait_until_no_element(page, '.loading-spinner')
+ ```
+ """
+ start_time = time.time()
+
+ while (time.time() - start_time) < timeout_seconds:
+ try:
+ element = page.locator(selector).first
+ if not element.is_visible(timeout=500):
+ return True
+ except:
+ return True # Element not found = disappeared
+
+ time.sleep(0.3)
+
+ return False
+
+
+def combined_wait(page: Page, timeout_seconds: int = 10) -> bool:
+ """
+ Comprehensive wait combining multiple strategies for maximum reliability.
+
+ Uses:
+ 1. Network idle
+ 2. No visible loading indicators
+ 3. DOM stability
+ 4. Brief settling time
+
+ Args:
+ page: Playwright Page object
+ timeout_seconds: Maximum time to wait
+
+ Returns:
+ True if all conditions met, False if timeout
+
+ Example:
+ ```python
+ page.click('button#submit')
+ combined_wait(page) # Wait for everything to settle
+ ```
+ """
+ start_time = time.time()
+
+ # Network idle
+ try:
+ page.wait_for_load_state('networkidle', timeout=timeout_seconds * 1000)
+ except:
+ pass
+
+ # Wait for common loading indicators to disappear
+ loading_selectors = [
+ '.loading',
+ '.spinner',
+ '[data-loading="true"]',
+ '[aria-busy="true"]',
+ ]
+
+ for selector in loading_selectors:
+ wait_until_no_element(page, selector, timeout_seconds=3)
+
+ # Final settling time
+ time.sleep(1)
+
+ return (time.time() - start_time) < timeout_seconds
diff --git a/claude-code/utils/git-worktree-utils.sh b/claude-code/utils/git-worktree-utils.sh
new file mode 100755
index 0000000..3e9b9f8
--- /dev/null
+++ b/claude-code/utils/git-worktree-utils.sh
@@ -0,0 +1,111 @@
+#!/bin/bash
+
+# ABOUTME: Git worktree utilities for agent workspace isolation
+
+set -euo pipefail
+
+# Create agent worktree with isolated branch
+create_agent_worktree() {
+ local AGENT_ID=$1
+ local BASE_BRANCH=${2:-$(git branch --show-current)}
+ local TASK_SLUG=${3:-""}
+
+ # Build directory name with optional task slug
+ if [ -n "$TASK_SLUG" ]; then
+ local WORKTREE_DIR="worktrees/agent-${AGENT_ID}-${TASK_SLUG}"
+ else
+ local WORKTREE_DIR="worktrees/agent-${AGENT_ID}"
+ fi
+
+ local BRANCH_NAME="agent/agent-${AGENT_ID}"
+
+ # Create worktrees directory if needed
+ mkdir -p worktrees
+
+ # Create worktree with new branch (redirect git output to stderr)
+ git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "$BASE_BRANCH" >&2
+
+ # Echo only the directory path to stdout
+ echo "$WORKTREE_DIR"
+}
+
+# Remove agent worktree
+cleanup_agent_worktree() {
+ local AGENT_ID=$1
+ local FORCE=${2:-false}
+
+ # Find worktree directory (may have task slug suffix)
+ local WORKTREE_DIR=$(find worktrees -type d -name "agent-${AGENT_ID}*" 2>/dev/null | head -1)
+ local BRANCH_NAME="agent/agent-${AGENT_ID}"
+
+ if [ -z "$WORKTREE_DIR" ] || [ ! -d "$WORKTREE_DIR" ]; then
+ echo "❌ Worktree not found for agent: $AGENT_ID"
+ return 1
+ fi
+
+ # Check for uncommitted changes
+ if ! git -C "$WORKTREE_DIR" diff --quiet 2>/dev/null; then
+ if [ "$FORCE" = false ]; then
+ echo "⚠️ Worktree has uncommitted changes. Use --force to remove anyway."
+ return 1
+ fi
+ fi
+
+ # Remove worktree
+ git worktree remove "$WORKTREE_DIR" $( [ "$FORCE" = true ] && echo "--force" )
+
+ # Delete branch (only if merged or forced)
+ git branch -d "$BRANCH_NAME" 2>/dev/null || \
+ ( [ "$FORCE" = true ] && git branch -D "$BRANCH_NAME" )
+}
+
+# List all agent worktrees
+list_agent_worktrees() {
+ git worktree list | grep "worktrees/agent-" || echo "No agent worktrees found"
+}
+
+# Merge agent work into current branch
+merge_agent_work() {
+ local AGENT_ID=$1
+ local BRANCH_NAME="agent/agent-${AGENT_ID}"
+
+ if ! git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
+ echo "❌ Branch not found: $BRANCH_NAME"
+ return 1
+ fi
+
+ git merge "$BRANCH_NAME"
+}
+
+# Check if worktree exists
+worktree_exists() {
+ local AGENT_ID=$1
+ local WORKTREE_DIR=$(find worktrees -type d -name "agent-${AGENT_ID}*" 2>/dev/null | head -1)
+
+ [ -n "$WORKTREE_DIR" ] && [ -d "$WORKTREE_DIR" ]
+}
+
+# Main CLI (only run if executed directly, not sourced)
+if [ "${BASH_SOURCE[0]:-}" = "${0:-}" ]; then
+ case "${1:-help}" in
+ create)
+ create_agent_worktree "$2" "${3:-}" "${4:-}"
+ ;;
+ cleanup)
+ cleanup_agent_worktree "$2" "${3:-false}"
+ ;;
+ list)
+ list_agent_worktrees
+ ;;
+ merge)
+ merge_agent_work "$2"
+ ;;
+ exists)
+ worktree_exists "$2"
+ ;;
+ *)
+ echo "Usage: git-worktree-utils.sh {create|cleanup|list|merge|exists} [args]"
+ exit 1
+ ;;
+ esac
+fi
diff --git a/claude-code/utils/orchestrator-agent.sh b/claude-code/utils/orchestrator-agent.sh
new file mode 100755
index 0000000..4724dac
--- /dev/null
+++ b/claude-code/utils/orchestrator-agent.sh
@@ -0,0 +1,120 @@
+#!/bin/bash
+
+# Agent Lifecycle Management Utility
+# Handles agent spawning, status detection, and termination
+
+set -euo pipefail
+
+# Source the spawn-agent logic
+SPAWN_AGENT_CMD="${HOME}/.claude/commands/spawn-agent.md"
+
+# detect_agent_status
+# Detects agent status from tmux output
+detect_agent_status() {
+ local tmux_session="$1"
+
+ if ! tmux has-session -t "$tmux_session" 2>/dev/null; then
+ echo "killed"
+ return 0
+ fi
+
+ local output=$(tmux capture-pane -t "$tmux_session" -p -S -100 2>/dev/null || echo "")
+
+ # Check for completion indicators
+ if echo "$output" | grep -qiE "complete|done|finished|✅.*complete"; then
+ if echo "$output" | grep -qE "git.*commit|Commit.*created"; then
+ echo "complete"
+ return 0
+ fi
+ fi
+
+ # Check for failure indicators
+ if echo "$output" | grep -qiE "error|failed|❌|fatal"; then
+ echo "failed"
+ return 0
+ fi
+
+ # Check for idle (no recent activity)
+ local last_line=$(echo "$output" | tail -1)
+ if echo "$last_line" | grep -qE "^>|^│|^─|Style:|bypass permissions"; then
+ echo "idle"
+ return 0
+ fi
+
+ # Active by default
+ echo "active"
+}
+
+# check_idle_timeout
+# Checks if agent has been idle too long
+check_idle_timeout() {
+ local session_id="$1"
+ local agent_id="$2"
+ local timeout_minutes="$3"
+
+ # Get agent's last_updated timestamp
+ local last_updated=$(~/.claude/utils/orchestrator-state.sh get-agent "$session_id" "$agent_id" | jq -r '.last_updated // empty')
+
+ if [ -z "$last_updated" ]; then
+ echo "false"
+ return 0
+ fi
+
+ local now=$(date +%s)
+ local last=$(date -j -f "%Y-%m-%dT%H:%M:%S" "${last_updated:0:19}" +%s 2>/dev/null || echo "$now")
+ local diff=$(( (now - last) / 60 ))
+
+ if [ "$diff" -gt "$timeout_minutes" ]; then
+ echo "true"
+ else
+ echo "false"
+ fi
+}
+
+# kill_agent
+# Kills an agent tmux session
+kill_agent() {
+ local tmux_session="$1"
+
+ if tmux has-session -t "$tmux_session" 2>/dev/null; then
+ tmux kill-session -t "$tmux_session"
+ echo "Killed agent session: $tmux_session"
+ fi
+}
+
+# extract_cost_from_tmux
+# Extracts cost from Claude status bar in tmux
+extract_cost_from_tmux() {
+ local tmux_session="$1"
+
+ local output=$(tmux capture-pane -t "$tmux_session" -p -S -50 2>/dev/null || echo "")
+
+ # Look for "Cost: $X.XX" pattern
+ local cost=$(echo "$output" | grep -oE 'Cost:\s*\$[0-9]+\.[0-9]{2}' | tail -1 | grep -oE '[0-9]+\.[0-9]{2}')
+
+ echo "${cost:-0.00}"
+}
+
+case "${1:-}" in
+ detect-status)
+ detect_agent_status "$2"
+ ;;
+ check-idle)
+ check_idle_timeout "$2" "$3" "$4"
+ ;;
+ kill)
+ kill_agent "$2"
+ ;;
+ extract-cost)
+ extract_cost_from_tmux "$2"
+ ;;
+ *)
+ echo "Usage: orchestrator-agent.sh [args...]"
+ echo "Commands:"
+ echo " detect-status "
+ echo " check-idle "
+ echo " kill "
+ echo " extract-cost "
+ exit 1
+ ;;
+esac
diff --git a/claude-code/utils/orchestrator-dag.sh b/claude-code/utils/orchestrator-dag.sh
new file mode 100755
index 0000000..b0b9c03
--- /dev/null
+++ b/claude-code/utils/orchestrator-dag.sh
@@ -0,0 +1,125 @@
+#!/bin/bash
+
+# DAG (Directed Acyclic Graph) Utility
+# Handles dependency resolution and wave calculation
+
+set -euo pipefail
+
+STATE_DIR="${HOME}/.claude/orchestration/state"
+
+# topological_sort
+# Returns nodes in topological order (waves)
+topological_sort() {
+ local dag_file="$1"
+
+ # Extract nodes and edges
+ local nodes=$(jq -r '.nodes | keys[]' "$dag_file")
+ local edges=$(jq -r '.edges' "$dag_file")
+
+ # Calculate in-degree for each node
+ declare -A indegree
+ for node in $nodes; do
+ local deps=$(jq -r --arg n "$node" '.edges[] | select(.to == $n) | .from' "$dag_file" | wc -l)
+ indegree[$node]=$deps
+ done
+
+ # Topological sort using Kahn's algorithm
+ local wave=1
+ local result=""
+
+ while [ ${#indegree[@]} -gt 0 ]; do
+ local wave_nodes=""
+
+ # Find all nodes with indegree 0
+ for node in "${!indegree[@]}"; do
+ if [ "${indegree[$node]}" -eq 0 ]; then
+ wave_nodes="$wave_nodes $node"
+ fi
+ done
+
+ if [ -z "$wave_nodes" ]; then
+ echo "Error: Cycle detected in DAG" >&2
+ return 1
+ fi
+
+ # Output wave
+ echo "$wave:$wave_nodes"
+
+ # Remove processed nodes and update indegrees
+ for node in $wave_nodes; do
+ unset indegree[$node]
+
+ # Decrease indegree for dependent nodes
+ local dependents=$(jq -r --arg n "$node" '.edges[] | select(.from == $n) | .to' "$dag_file")
+ for dep in $dependents; do
+ if [ -n "${indegree[$dep]:-}" ]; then
+ indegree[$dep]=$((indegree[$dep] - 1))
+ fi
+ done
+ done
+
+ ((wave++))
+ done
+}
+
+# check_dependencies
+# Checks if all dependencies for a node are satisfied
+check_dependencies() {
+ local dag_file="$1"
+ local node_id="$2"
+
+ local deps=$(jq -r --arg n "$node_id" '.edges[] | select(.to == $n) | .from' "$dag_file")
+
+ if [ -z "$deps" ]; then
+ echo "true"
+ return 0
+ fi
+
+ # Check if all dependencies are complete
+ for dep in $deps; do
+ local status=$(jq -r --arg n "$dep" '.nodes[$n].status' "$dag_file")
+ if [ "$status" != "complete" ]; then
+ echo "false"
+ return 1
+ fi
+ done
+
+ echo "true"
+}
+
+# get_next_wave
+# Gets the next wave of nodes ready to execute
+get_next_wave() {
+ local dag_file="$1"
+
+ local nodes=$(jq -r '.nodes | to_entries[] | select(.value.status == "pending") | .key' "$dag_file")
+
+ local wave_nodes=""
+ for node in $nodes; do
+ if [ "$(check_dependencies "$dag_file" "$node")" = "true" ]; then
+ wave_nodes="$wave_nodes $node"
+ fi
+ done
+
+ echo "$wave_nodes" | tr -s ' '
+}
+
+case "${1:-}" in
+ topo-sort)
+ topological_sort "$2"
+ ;;
+ check-deps)
+ check_dependencies "$2" "$3"
+ ;;
+ next-wave)
+ get_next_wave "$2"
+ ;;
+ *)
+ echo "Usage: orchestrator-dag.sh [args...]"
+ echo "Commands:"
+ echo " topo-sort "
+ echo " check-deps "
+ echo " next-wave "
+ exit 1
+ ;;
+esac
diff --git a/claude-code/utils/orchestrator-state.sh b/claude-code/utils/orchestrator-state.sh
new file mode 100755
index 0000000..40b9a57
--- /dev/null
+++ b/claude-code/utils/orchestrator-state.sh
@@ -0,0 +1,431 @@
+#!/bin/bash
+
+# Orchestrator State Management Utility
+# Manages sessions.json, completed.json, and DAG state files
+
+set -euo pipefail
+
+# Paths
+STATE_DIR="${HOME}/.claude/orchestration/state"
+SESSIONS_FILE="${STATE_DIR}/sessions.json"
+COMPLETED_FILE="${STATE_DIR}/completed.json"
+CONFIG_FILE="${STATE_DIR}/config.json"
+
+# Ensure jq is available
+if ! command -v jq &> /dev/null; then
+ echo "Error: jq is required but not installed. Install with: brew install jq"
+ exit 1
+fi
+
+# ============================================================================
+# Session Management Functions
+# ============================================================================
+
+# create_session [config_json]
+# Creates a new orchestration session
+create_session() {
+ local session_id="$1"
+ local tmux_session="$2"
+ local custom_config="${3:-{}}"
+
+ # Load default config
+ local default_config=$(jq -r '.orchestrator' "$CONFIG_FILE")
+
+ # Merge custom config with defaults
+ local merged_config=$(echo "$default_config" | jq ". + $custom_config")
+
+ # Create session object
+ local session=$(cat < "$SESSIONS_FILE"
+
+ echo "$session_id"
+}
+
+# get_session
+# Retrieves a session by ID
+get_session() {
+ local session_id="$1"
+ jq -r ".active_sessions[] | select(.session_id == \"$session_id\")" "$SESSIONS_FILE"
+}
+
+# update_session
+# Updates a session with new data (merges)
+update_session() {
+ local session_id="$1"
+ local update="$2"
+
+ local updated=$(jq \
+ --arg id "$session_id" \
+ --argjson upd "$update" \
+ '(.active_sessions[] | select(.session_id == $id)) |= (. + $upd) | .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# update_session_status
+# Updates session status
+update_session_status() {
+ local session_id="$1"
+ local status="$2"
+
+ update_session "$session_id" "{\"status\": \"$status\"}"
+}
+
+# archive_session
+# Moves session from active to completed
+archive_session() {
+ local session_id="$1"
+
+ # Get session data
+ local session=$(get_session "$session_id")
+
+ if [ -z "$session" ]; then
+ echo "Error: Session $session_id not found"
+ return 1
+ fi
+
+ # Mark as complete with end time
+ local completed_session=$(echo "$session" | jq ". + {\"completed_at\": \"$(date -Iseconds)\"}")
+
+ # Add to completed sessions
+ local updated_completed=$(jq ".completed_sessions += [$completed_session] | .last_updated = \"$(date -Iseconds)\"" "$COMPLETED_FILE")
+ echo "$updated_completed" > "$COMPLETED_FILE"
+
+ # Update totals
+ local total_cost=$(echo "$completed_session" | jq -r '.total_cost_usd')
+ local updated_totals=$(jq \
+ --arg cost "$total_cost" \
+ '.total_cost_usd += ($cost | tonumber) | .total_agents_spawned += 1' \
+ "$COMPLETED_FILE")
+ echo "$updated_totals" > "$COMPLETED_FILE"
+
+ # Remove from active sessions
+ local updated_active=$(jq \
+ --arg id "$session_id" \
+ '.active_sessions = [.active_sessions[] | select(.session_id != $id)] | .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+ echo "$updated_active" > "$SESSIONS_FILE"
+
+ echo "Session $session_id archived"
+}
+
+# list_active_sessions
+# Lists all active sessions
+list_active_sessions() {
+ jq -r '.active_sessions[] | .session_id' "$SESSIONS_FILE"
+}
+
+# ============================================================================
+# Agent Management Functions
+# ============================================================================
+
+# add_agent
+# Adds an agent to a session
+add_agent() {
+ local session_id="$1"
+ local agent_id="$2"
+ local agent_config="$3"
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ --argjson cfg "$agent_config" \
+ '(.active_sessions[] | select(.session_id == $sid).agents[$aid]) = $cfg | .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# update_agent_status
+# Updates an agent's status
+update_agent_status() {
+ local session_id="$1"
+ local agent_id="$2"
+ local status="$3"
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ --arg st "$status" \
+ '(.active_sessions[] | select(.session_id == $sid).agents[$aid].status) = $st |
+ (.active_sessions[] | select(.session_id == $sid).agents[$aid].last_updated) = "'$(date -Iseconds)'" |
+ .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# update_agent_cost
+# Updates an agent's cost
+update_agent_cost() {
+ local session_id="$1"
+ local agent_id="$2"
+ local cost_usd="$3"
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ --arg cost "$cost_usd" \
+ '(.active_sessions[] | select(.session_id == $sid).agents[$aid].cost_usd) = ($cost | tonumber) |
+ .last_updated = "'$(date -Iseconds)'"' \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+
+ # Update session total cost
+ update_session_total_cost "$session_id"
+}
+
+# update_session_total_cost
+# Recalculates and updates session total cost
+update_session_total_cost() {
+ local session_id="$1"
+
+ local total=$(jq -r \
+ --arg sid "$session_id" \
+ '(.active_sessions[] | select(.session_id == $sid).agents | to_entries | map(.value.cost_usd // 0) | add) // 0' \
+ "$SESSIONS_FILE")
+
+ update_session "$session_id" "{\"total_cost_usd\": $total}"
+}
+
+# get_agent
+# Gets agent data
+get_agent() {
+ local session_id="$1"
+ local agent_id="$2"
+
+ jq -r \
+ --arg sid "$session_id" \
+ --arg aid "$agent_id" \
+ '.active_sessions[] | select(.session_id == $sid).agents[$aid]' \
+ "$SESSIONS_FILE"
+}
+
+# list_agents
+# Lists all agents in a session
+list_agents() {
+ local session_id="$1"
+
+ jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).agents | keys[]' \
+ "$SESSIONS_FILE"
+}
+
+# ============================================================================
+# Wave Management Functions
+# ============================================================================
+
+# add_wave
+# Adds a wave to the session
+add_wave() {
+ local session_id="$1"
+ local wave_number="$2"
+ local agent_ids="$3" # JSON array like '["agent-1", "agent-2"]'
+
+ local wave=$(cat < "$SESSIONS_FILE"
+}
+
+# update_wave_status
+# Updates wave status
+update_wave_status() {
+ local session_id="$1"
+ local wave_number="$2"
+ local status="$3"
+
+ local timestamp_field=""
+ if [ "$status" = "active" ]; then
+ timestamp_field="started_at"
+ elif [ "$status" = "complete" ] || [ "$status" = "failed" ]; then
+ timestamp_field="completed_at"
+ fi
+
+ local jq_filter='(.active_sessions[] | select(.session_id == $sid).waves[] | select(.wave_number == ($wn | tonumber)).status) = $st'
+
+ if [ -n "$timestamp_field" ]; then
+ jq_filter="$jq_filter | (.active_sessions[] | select(.session_id == \$sid).waves[] | select(.wave_number == (\$wn | tonumber)).$timestamp_field) = \"$(date -Iseconds)\""
+ fi
+
+ jq_filter="$jq_filter | .last_updated = \"$(date -Iseconds)\""
+
+ local updated=$(jq \
+ --arg sid "$session_id" \
+ --arg wn "$wave_number" \
+ --arg st "$status" \
+ "$jq_filter" \
+ "$SESSIONS_FILE")
+
+ echo "$updated" > "$SESSIONS_FILE"
+}
+
+# get_current_wave
+# Gets the current active or next pending wave number
+get_current_wave() {
+ local session_id="$1"
+
+ # First check for active waves
+ local active_wave=$(jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).waves[] | select(.status == "active") | .wave_number' \
+ "$SESSIONS_FILE" | head -1)
+
+ if [ -n "$active_wave" ]; then
+ echo "$active_wave"
+ return
+ fi
+
+ # Otherwise get first pending wave
+ local pending_wave=$(jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).waves[] | select(.status == "pending") | .wave_number' \
+ "$SESSIONS_FILE" | head -1)
+
+ echo "${pending_wave:-0}"
+}
+
+# ============================================================================
+# Utility Functions
+# ============================================================================
+
+# check_budget_limit
+# Checks if session is within budget limits
+check_budget_limit() {
+ local session_id="$1"
+
+ local max_budget=$(jq -r '.resource_limits.max_budget_usd' "$CONFIG_FILE")
+ local warn_percent=$(jq -r '.resource_limits.warn_at_percent' "$CONFIG_FILE")
+ local stop_percent=$(jq -r '.resource_limits.hard_stop_at_percent' "$CONFIG_FILE")
+
+ local current_cost=$(jq -r \
+ --arg sid "$session_id" \
+ '.active_sessions[] | select(.session_id == $sid).total_cost_usd' \
+ "$SESSIONS_FILE")
+
+ local percent=$(echo "scale=2; ($current_cost / $max_budget) * 100" | bc)
+
+ if (( $(echo "$percent >= $stop_percent" | bc -l) )); then
+ echo "STOP"
+ return 1
+ elif (( $(echo "$percent >= $warn_percent" | bc -l) )); then
+ echo "WARN"
+ return 0
+ else
+ echo "OK"
+ return 0
+ fi
+}
+
+# pretty_print_session
+# Pretty prints a session
+pretty_print_session() {
+ local session_id="$1"
+ get_session "$session_id" | jq '.'
+}
+
+# ============================================================================
+# Main CLI Interface
+# ============================================================================
+
+case "${1:-}" in
+ create)
+ create_session "$2" "$3" "${4:-{}}"
+ ;;
+ get)
+ get_session "$2"
+ ;;
+ update)
+ update_session "$2" "$3"
+ ;;
+ archive)
+ archive_session "$2"
+ ;;
+ list)
+ list_active_sessions
+ ;;
+ add-agent)
+ add_agent "$2" "$3" "$4"
+ ;;
+ update-agent-status)
+ update_agent_status "$2" "$3" "$4"
+ ;;
+ update-agent-cost)
+ update_agent_cost "$2" "$3" "$4"
+ ;;
+ get-agent)
+ get_agent "$2" "$3"
+ ;;
+ list-agents)
+ list_agents "$2"
+ ;;
+ add-wave)
+ add_wave "$2" "$3" "$4"
+ ;;
+ update-wave-status)
+ update_wave_status "$2" "$3" "$4"
+ ;;
+ get-current-wave)
+ get_current_wave "$2"
+ ;;
+ check-budget)
+ check_budget_limit "$2"
+ ;;
+ print)
+ pretty_print_session "$2"
+ ;;
+ *)
+ echo "Usage: orchestrator-state.sh [args...]"
+ echo ""
+ echo "Commands:"
+ echo " create [config_json]"
+ echo " get "
+ echo " update "
+ echo " archive "
+ echo " list"
+ echo " add-agent