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 " + 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