Skip to content

Instantly share code, notes, and snippets.

@hilash
Created January 28, 2026 17:52
Show Gist options
  • Select an option

  • Save hilash/bf86764d88d1ac02592eb6d09b7688ec to your computer and use it in GitHub Desktop.

Select an option

Save hilash/bf86764d88d1ac02592eb6d09b7688ec to your computer and use it in GitHub Desktop.
Parallel task executor for Claude Code

Task Orchestrator

Parallel task executor that runs multiple Claude Code instances simultaneously.

Quick Start

# 1. Initialize task directories
./scripts/tasks_orchestrator.sh --init

# 2. Create a task file
cat > tasks/pending/my-task.json << 'EOF'
{
  "task_name": "Add Search Feature",
  "task_description": "Add a search bar to the header that filters items in real-time",
  "category": "Feature"
}
EOF

# 3. Run the orchestrator
./scripts/tasks_orchestrator.sh

Writing a Task

Create a JSON file with these fields:

{
  "task_name": "Short task name",
  "task_description": "Detailed description of what needs to be done",
  "category": "Optional category for grouping"
}

Required Fields

Field Description
task_name Short name shown in logs and window titles
task_description Full description - this becomes the prompt for Claude

Optional Fields

Field Description
category Category for grouping (Analytics, UI, Backend, etc.)
prompt Custom prompt to override auto-generated one

Examples

Feature Task:

{
  "task_name": "Add Dark Mode Toggle",
  "task_description": "Add a dark mode toggle to the settings page. Use CSS variables for theming. Store preference in localStorage.",
  "category": "UI"
}

Bug Fix Task:

{
  "task_name": "Fix Login Redirect",
  "task_description": "Users are not redirected to dashboard after login. Check the auth callback handler.",
  "category": "Bug"
}

Refactoring Task:

{
  "task_name": "Extract API Client",
  "task_description": "Extract all fetch calls into a dedicated API client module with proper error handling and retry logic.",
  "category": "Refactor"
}

Custom Prompt Task:

{
  "task_name": "Code Review",
  "task_description": "Review recent changes",
  "category": "Review",
  "prompt": "Review all uncommitted changes in this repo. Check for:\n1. Security issues\n2. Performance problems\n3. Code style violations\n\nCreate a REVIEW.md file with your findings."
}

Running the Orchestrator

./scripts/tasks_orchestrator.sh [options]

Options

Option Description
-p N, --max-parallel N Run N workers in parallel (default: 10)
-t PATH, --tasks-dir PATH Tasks directory (default: ./tasks)
-w PATH, --working-dir PATH Working directory for Claude
-v, --verbose Enable audio notifications
-s, --status Show task counts without running
-r, --reset-failed Move failed tasks back to pending
--reset-wip Move stuck WIP tasks back to pending
--init Initialize task directories
-h, --help Show help

Examples

# Run with 5 parallel workers
./scripts/tasks_orchestrator.sh -p 5

# Check status
./scripts/tasks_orchestrator.sh --status

# Retry failed tasks
./scripts/tasks_orchestrator.sh --reset-failed
./scripts/tasks_orchestrator.sh

# Use custom directories
./scripts/tasks_orchestrator.sh -t ./my-tasks -w /path/to/project

Directory Structure

tasks/
├── pending/      # Tasks waiting to run (put new tasks here)
├── wip/          # Currently running tasks
├── completed/    # Successfully finished tasks
├── failed/       # Failed tasks (use --reset-failed to retry)
├── locks/        # Lock files (managed automatically)
└── logs/         # Worker logs and orchestrator logs

Task Lifecycle

pending/  →  wip/  →  completed/
                  →  failed/
  1. Put task JSON files in tasks/pending/
  2. Orchestrator claims task (moves to wip/)
  3. Claude Code runs in a new iTerm2 window
  4. On success → moves to completed/
  5. On failure → moves to failed/

Fields Added by Orchestrator

When a task is processed, the orchestrator adds these fields:

{
  "task_name": "My Task",
  "task_description": "...",
  "category": "...",
  "status": "completed",
  "started_at": "2026-01-26T10:00:00Z",
  "completed_at": "2026-01-26T10:15:00Z",
  "worker_id": "3"
}

Tips

  • Task names should be short - they appear in iTerm2 window titles
  • Task descriptions can be long - they become the Claude prompt
  • Use categories to group related tasks
  • Check logs/ for debugging if tasks fail
  • Use --status to monitor progress without interrupting
  • Press Ctrl+C for graceful shutdown (waits for current workers)

Requirements

  • macOS with iTerm2
  • jq for JSON processing (brew install jq)
  • Claude Code CLI installed (npm install -g @anthropic-ai/claude-code)
#!/bin/bash
set -e
# ============================================
# Task Orchestrator
# Parallel task executor for Claude Code
#
# Spawns multiple Claude Code instances in iTerm2 windows,
# each working on a separate task from a directory-based queue.
#
# Features:
# - Directory-based task queue (pending → wip → completed/failed)
# - Atomic task claiming with mkdir locks (macOS compatible)
# - Worker pool pattern with configurable parallelism
# - Graceful shutdown on Ctrl+C
# - Auto-respawn workers when tasks complete
#
# Requirements: macOS, iTerm2, jq, Claude Code CLI
# Compatible with bash 3.2+ (macOS default)
#
# By Hila Shmuel @hilashmuel
# MIT License - https://opensource.org/licenses/MIT
# ============================================
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
WHITE='\033[1;37m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Configuration (can be overridden via CLI)
TASKS_DIR="${TASKS_DIR:-./tasks}"
WORKING_DIR="${WORKING_DIR:-$(pwd)}"
MAX_PARALLEL=${MAX_PARALLEL:-10}
VERBOSE=${VERBOSE:-false}
# Timestamp for this run
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
# ============================================
# Helper Functions
# ============================================
print_banner() {
echo -e "${CYAN}"
echo "+=================================================================+"
echo "| |"
echo "| Task Orchestrator |"
echo "| Parallel Claude Code Task Runner |"
echo "| |"
echo "+=================================================================+"
echo -e "${NC}"
}
print_separator() {
echo -e "${BLUE}------------------------------------------------------------------${NC}"
}
log_info() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${WHITE}[$timestamp]${NC} ${BLUE}INFO:${NC} $message"
[ -f "$LOG_FILE" ] && echo "[$timestamp] INFO: $message" >> "$LOG_FILE"
return 0
}
log_success() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${WHITE}[$timestamp]${NC} ${GREEN}SUCCESS:${NC} $message"
[ -f "$LOG_FILE" ] && echo "[$timestamp] SUCCESS: $message" >> "$LOG_FILE"
return 0
}
log_warning() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${WHITE}[$timestamp]${NC} ${YELLOW}WARNING:${NC} $message"
[ -f "$LOG_FILE" ] && echo "[$timestamp] WARNING: $message" >> "$LOG_FILE"
return 0
}
log_error() {
local message="$1"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${WHITE}[$timestamp]${NC} ${RED}ERROR:${NC} $message"
[ -f "$LOG_FILE" ] && echo "[$timestamp] ERROR: $message" >> "$LOG_FILE"
return 0
}
log_worker() {
local worker="$1"
local message="$2"
local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${WHITE}[$timestamp]${NC} ${PURPLE}WORKER $worker:${NC} $message"
[ -f "$LOG_FILE" ] && echo "[$timestamp] WORKER $worker: $message" >> "$LOG_FILE"
return 0
}
verbose_say() {
if [ "$VERBOSE" = true ]; then
say "$1"
fi
}
# ============================================
# Directory Management
# ============================================
init_task_dirs() {
mkdir -p "$TASKS_DIR/pending"
mkdir -p "$TASKS_DIR/wip"
mkdir -p "$TASKS_DIR/completed"
mkdir -p "$TASKS_DIR/failed"
mkdir -p "$TASKS_DIR/locks"
mkdir -p "$TASKS_DIR/logs"
log_success "Initialized task directories at $TASKS_DIR"
}
ensure_task_dirs() {
if [ ! -d "$TASKS_DIR/pending" ]; then
log_error "Task directories not found. Run with --init first."
exit 1
fi
}
# ============================================
# Task Discovery (Directory-based, FIFO order)
# ============================================
get_pending_tasks() {
# Returns list of pending task files sorted by modification time (FIFO)
find "$TASKS_DIR/pending" -name "*.json" -type f 2>/dev/null | \
xargs ls -t 2>/dev/null | \
tail -r 2>/dev/null || \
find "$TASKS_DIR/pending" -name "*.json" -type f 2>/dev/null | sort
}
get_pending_count() {
find "$TASKS_DIR/pending" -name "*.json" -type f 2>/dev/null | wc -l | tr -d ' '
}
get_wip_count() {
find "$TASKS_DIR/wip" -name "*.json" -type f 2>/dev/null | wc -l | tr -d ' '
}
get_completed_count() {
find "$TASKS_DIR/completed" -name "*.json" -type f 2>/dev/null | wc -l | tr -d ' '
}
get_failed_count() {
find "$TASKS_DIR/failed" -name "*.json" -type f 2>/dev/null | wc -l | tr -d ' '
}
# ============================================
# Task Claiming with mkdir atomic lock (macOS compatible)
# ============================================
claim_task() {
local task_path="$1"
local task_name=$(basename "$task_path")
local lock_dir="$TASKS_DIR/locks/${task_name}.lock"
local wip_path="$TASKS_DIR/wip/$task_name"
# Try to acquire lock using mkdir (atomic on all systems)
if ! mkdir "$lock_dir" 2>/dev/null; then
# Another process has the lock
return 1
fi
# We have the lock - cleanup on exit
trap "rmdir '$lock_dir' 2>/dev/null" EXIT
# Double-check file still exists in pending
if [ ! -f "$task_path" ]; then
rmdir "$lock_dir" 2>/dev/null
return 1
fi
# Move to wip
if ! mv "$task_path" "$wip_path" 2>/dev/null; then
rmdir "$lock_dir" 2>/dev/null
return 1
fi
# Update task with started_at
local tmp_file=$(mktemp)
if jq --arg ts "$(date -Iseconds)" \
'.started_at = $ts | .status = "in_progress"' \
"$wip_path" > "$tmp_file" 2>/dev/null; then
mv "$tmp_file" "$wip_path"
fi
# Release lock
rmdir "$lock_dir" 2>/dev/null
trap - EXIT
echo "$wip_path"
}
complete_task() {
local wip_path="$1"
local task_name=$(basename "$wip_path")
local completed_path="$TASKS_DIR/completed/$task_name"
# Update task with completion info
local tmp_file=$(mktemp)
jq --arg ts "$(date -Iseconds)" \
'.completed_at = $ts | .status = "completed"' \
"$wip_path" > "$tmp_file" && mv "$tmp_file" "$wip_path"
mv "$wip_path" "$completed_path" 2>/dev/null
}
fail_task() {
local wip_path="$1"
local task_name=$(basename "$wip_path")
local failed_path="$TASKS_DIR/failed/$task_name"
# Update task with failure info
local tmp_file=$(mktemp)
jq --arg ts "$(date -Iseconds)" \
'.completed_at = $ts | .status = "failed"' \
"$wip_path" > "$tmp_file" && mv "$tmp_file" "$wip_path"
mv "$wip_path" "$failed_path" 2>/dev/null
}
# ============================================
# Open iTerm2 Window for Worker
# ============================================
open_worker_window() {
local worker_id="$1"
local script_path="$2"
local window_name="$3"
# Sanitize window name for AppleScript
local safe_name=$(echo "$window_name" | sed 's/[\/()]/-/g; s/--*/-/g; s/^-//; s/-$//')
osascript <<EOF
tell application "iTerm2"
activate
set newWindow to (create window with default profile)
tell current session of newWindow
set name to "$safe_name"
write text "source $script_path"
end tell
end tell
EOF
}
# ============================================
# Generate Worker Prompt
# ============================================
generate_worker_prompt() {
local task_file="$1"
local worker_id="$2"
# Read task JSON
local task_name=$(jq -r '.task_name // "Unnamed Task"' "$task_file")
local task_description=$(jq -r '.task_description // ""' "$task_file")
local category=$(jq -r '.category // "General"' "$task_file")
# Check if custom prompt exists
local custom_prompt=$(jq -r '.prompt // ""' "$task_file")
if [ -n "$custom_prompt" ] && [ "$custom_prompt" != "null" ]; then
echo "$custom_prompt"
return
fi
# Generate default prompt from task fields
cat <<PROMPT
You are WORKER $worker_id executing a task.
## Task: $task_name
**Category:** $category
## Description
$task_description
## Instructions
1. Read and understand the task description above
2. Implement the required changes
3. Test your changes if applicable
4. Commit your changes with a descriptive message
## When Complete
Output this marker when done:
<worker-$worker_id-complete>DONE</worker-$worker_id-complete>
REMEMBER: You are WORKER $worker_id working on "$task_name"
PROMPT
}
# ============================================
# Spawn a Single Worker
# ============================================
spawn_worker() {
local worker_slot="$1"
local task_path="$2"
local global_worker_id="$3"
# Claim the task
local wip_path=$(claim_task "$task_path")
if [ -z "$wip_path" ]; then
log_warning "Failed to claim task: $(basename "$task_path")"
return 1
fi
# Update task with worker_id
local tmp_file=$(mktemp)
jq --arg wid "$global_worker_id" '.worker_id = $wid' "$wip_path" > "$tmp_file" && mv "$tmp_file" "$wip_path"
local task_name=$(jq -r '.task_name // "Unnamed"' "$wip_path")
local category=$(jq -r '.category // "General"' "$wip_path")
echo -e " ${PURPLE}Slot $worker_slot (Worker #$global_worker_id):${NC} $task_name"
echo -e " ${BLUE}Category:${NC} $category"
echo ""
# Create marker file for tracking
local marker_file="$TASKS_DIR/locks/.worker_slot_${worker_slot}_running_$TIMESTAMP"
echo "$wip_path" > "$marker_file"
# Generate prompt
local prompt=$(generate_worker_prompt "$wip_path" "$global_worker_id")
local prompt_file="$TASKS_DIR/logs/prompt_worker_${global_worker_id}.txt"
echo "$prompt" > "$prompt_file"
# Worker script
local worker_script="$TASKS_DIR/logs/worker_${global_worker_id}.sh"
log_worker "$worker_slot" "Starting: $task_name"
# Write worker script
cat > "$worker_script" << WORKER_EOF
#!/bin/bash
cd "$WORKING_DIR"
echo "Starting Claude for: $task_name"
echo "================================================"
echo "Category: $category"
echo "Worker: $global_worker_id"
echo "================================================"
# Marker file for completion detection
DONE_MARKER="$TASKS_DIR/logs/.done_worker_${global_worker_id}"
rm -f "\$DONE_MARKER"
# Create hook settings for Stop detection
HOOK_SETTINGS="$TASKS_DIR/logs/hooks_worker_${global_worker_id}.json"
cat > "\$HOOK_SETTINGS" << HOOKEOF
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "command",
"command": "touch \$DONE_MARKER"
}]
}]
}
}
HOOKEOF
# Read prompt
PROMPT=\$(cat "$prompt_file")
# Background monitor for completion
(
while [ ! -f "\$DONE_MARKER" ]; do
sleep 2
done
echo ""
echo "Claude finished. Closing in 5 seconds..."
sleep 5
pkill -f "claude.*hooks_worker_${global_worker_id}" 2>/dev/null
) &
MONITOR_PID=\$!
# Run Claude
claude --dangerously-skip-permissions --settings "\$HOOK_SETTINGS" "\$PROMPT"
# Cleanup monitor
kill \$MONITOR_PID 2>/dev/null
echo ""
echo "================================================"
# Handle completion
WIP_PATH="$wip_path"
TASKS_DIR="$TASKS_DIR"
if [ -f "\$DONE_MARKER" ]; then
echo "Worker $global_worker_id COMPLETED: $task_name"
# Move to completed
TASK_NAME=\$(basename "\$WIP_PATH")
TMP_FILE=\$(mktemp)
jq --arg ts "\$(date -Iseconds)" '.completed_at = \$ts | .status = "completed"' "\$WIP_PATH" > "\$TMP_FILE" && mv "\$TMP_FILE" "\$WIP_PATH"
mv "\$WIP_PATH" "\$TASKS_DIR/completed/\$TASK_NAME" 2>/dev/null && echo "Task marked as completed"
else
echo "Worker $global_worker_id FAILED: $task_name"
# Move to failed
TASK_NAME=\$(basename "\$WIP_PATH")
TMP_FILE=\$(mktemp)
jq --arg ts "\$(date -Iseconds)" '.completed_at = \$ts | .status = "failed"' "\$WIP_PATH" > "\$TMP_FILE" && mv "\$TMP_FILE" "\$WIP_PATH"
mv "\$WIP_PATH" "\$TASKS_DIR/failed/\$TASK_NAME" 2>/dev/null && echo "Task marked as failed"
fi
# Cleanup
rm -f "\$DONE_MARKER" "\$HOOK_SETTINGS" "$prompt_file" "$marker_file"
echo "Window will close in 5 seconds..."
sleep 5
exit 0
WORKER_EOF
chmod +x "$worker_script"
open_worker_window "$global_worker_id" "$worker_script" "Worker $global_worker_id - $task_name"
}
# ============================================
# Show Task Summary
# ============================================
show_task_summary() {
echo ""
echo -e "${BOLD}${CYAN}Task Summary:${NC}"
local pending=$(get_pending_count)
local wip=$(get_wip_count)
local completed=$(get_completed_count)
local failed=$(get_failed_count)
echo -e " ${YELLOW}Pending:${NC} $pending"
echo -e " ${PURPLE}In Progress:${NC} $wip"
echo -e " ${GREEN}Completed:${NC} $completed"
echo -e " ${RED}Failed:${NC} $failed"
echo ""
# Show pending tasks
if [ "$pending" -gt 0 ]; then
echo -e "${BOLD}${CYAN}Pending Tasks:${NC}"
for f in "$TASKS_DIR/pending"/*.json; do
if [ -f "$f" ]; then
local name=$(jq -r '.task_name // "Unnamed"' "$f")
local cat=$(jq -r '.category // "-"' "$f")
echo -e " - ${WHITE}$name${NC} ($cat)"
fi
done
echo ""
fi
}
# ============================================
# Reset Functions
# ============================================
reset_failed() {
echo "Resetting failed tasks to pending..."
local count=0
for f in "$TASKS_DIR/failed"/*.json; do
if [ -f "$f" ]; then
local name=$(basename "$f")
# Remove status fields
local tmp_file=$(mktemp)
jq 'del(.status, .started_at, .completed_at, .worker_id)' "$f" > "$tmp_file" && mv "$tmp_file" "$f"
mv "$f" "$TASKS_DIR/pending/$name"
echo " Reset: $name"
count=$((count + 1))
fi
done
echo "Reset $count task(s)."
}
reset_wip() {
echo "Resetting WIP tasks to pending..."
local count=0
for f in "$TASKS_DIR/wip"/*.json; do
if [ -f "$f" ]; then
local name=$(basename "$f")
# Remove status fields
local tmp_file=$(mktemp)
jq 'del(.status, .started_at, .completed_at, .worker_id)' "$f" > "$tmp_file" && mv "$tmp_file" "$f"
mv "$f" "$TASKS_DIR/pending/$name"
echo " Reset: $name"
count=$((count + 1))
fi
done
echo "Reset $count task(s)."
}
# ============================================
# Main Execution - Worker Pool Pattern
# ============================================
main() {
# Ensure we're using absolute paths
TASKS_DIR=$(cd "$TASKS_DIR" 2>/dev/null && pwd || echo "$TASKS_DIR")
WORKING_DIR=$(cd "$WORKING_DIR" 2>/dev/null && pwd || echo "$WORKING_DIR")
# Set up log file
LOG_FILE="$TASKS_DIR/logs/orchestrator_$TIMESTAMP.log"
mkdir -p "$TASKS_DIR/logs"
# Clean up old temp files
rm -f "$TASKS_DIR/locks"/.worker_slot_*_running_*
rm -f "$TASKS_DIR/logs"/worker_*.sh
rm -f "$TASKS_DIR/logs"/prompt_worker_*.txt
rm -f "$TASKS_DIR/logs"/hooks_worker_*.json
rm -f "$TASKS_DIR/logs"/.done_worker_*
print_banner
log_info "Starting Task Orchestrator"
log_info "Tasks directory: $TASKS_DIR"
log_info "Working directory: $WORKING_DIR"
log_info "Worker pool size: $MAX_PARALLEL"
verbose_say "Starting task orchestrator with $MAX_PARALLEL workers"
print_separator
show_task_summary
print_separator
# Check for pending tasks
TOTAL_TASKS=$(get_pending_count)
if [ "$TOTAL_TASKS" -eq 0 ]; then
log_success "No pending tasks to process!"
exit 0
fi
log_info "Found $TOTAL_TASKS pending task(s)"
verbose_say "Processing $TOTAL_TASKS tasks"
print_separator
echo ""
echo -e "${BOLD}${CYAN}WORKER POOL STATUS:${NC}"
echo -e " Pending Tasks: $TOTAL_TASKS"
echo -e " Pool Size: $MAX_PARALLEL"
print_separator
# State tracking
local global_worker_counter=0
local tasks_completed=0
local tasks_failed=0
# Graceful shutdown
STOP_SPAWNING=false
trap 'STOP_SPAWNING=true; echo ""; log_warning "Ctrl+C - stopping new tasks"; verbose_say "Stopping new tasks"' SIGINT
# Slot tracking
local SLOT_DIR="$TASKS_DIR/locks/slots_$TIMESTAMP"
mkdir -p "$SLOT_DIR"
# Initial spawn
echo ""
echo -e "${BOLD}${YELLOW}Starting workers...${NC}"
echo ""
local initial_workers=$MAX_PARALLEL
if [ "$initial_workers" -gt "$TOTAL_TASKS" ]; then
initial_workers=$TOTAL_TASKS
fi
local slot=1
for task_path in $(get_pending_tasks | head -$initial_workers); do
if [ -f "$task_path" ]; then
global_worker_counter=$((global_worker_counter + 1))
spawn_worker "$slot" "$task_path" "$global_worker_counter"
echo "$task_path" > "$SLOT_DIR/slot_${slot}_task"
slot=$((slot + 1))
# Delay between spawns
if [ $slot -le $initial_workers ]; then
sleep 5
fi
fi
done
print_separator
echo ""
echo -e "${BOLD}${YELLOW}Worker pool active! Monitoring...${NC}"
echo ""
verbose_say "Worker pool active"
# Monitor loop
local check_count=0
local max_check_time=14400 # 4 hours
local check_interval=5
while true; do
sleep $check_interval
check_count=$((check_count + 1))
local active_workers=0
local slots_to_respawn=""
# Check each slot
for ((s=1; s<=MAX_PARALLEL; s++)); do
local marker_file="$TASKS_DIR/locks/.worker_slot_${s}_running_$TIMESTAMP"
local slot_task_file="$SLOT_DIR/slot_${s}_task"
if [ -f "$marker_file" ]; then
active_workers=$((active_workers + 1))
elif [ -f "$slot_task_file" ]; then
# Worker finished
local original_task=$(cat "$slot_task_file")
local task_basename=$(basename "$original_task")
if [ -f "$TASKS_DIR/completed/$task_basename" ]; then
log_success "Slot $s completed: $task_basename"
tasks_completed=$((tasks_completed + 1))
elif [ -f "$TASKS_DIR/failed/$task_basename" ]; then
log_warning "Slot $s failed: $task_basename"
tasks_failed=$((tasks_failed + 1))
elif [ -f "$TASKS_DIR/wip/$task_basename" ]; then
# Worker crashed
log_warning "Slot $s crashed: $task_basename"
fail_task "$TASKS_DIR/wip/$task_basename"
tasks_failed=$((tasks_failed + 1))
fi
rm -f "$slot_task_file"
# Queue for respawn
if [ "$(get_pending_count)" -gt 0 ]; then
slots_to_respawn="$slots_to_respawn $s"
fi
fi
done
# Respawn workers
if [ "$STOP_SPAWNING" = false ]; then
for slot in $slots_to_respawn; do
local next_task=$(get_pending_tasks | head -1)
if [ -n "$next_task" ] && [ -f "$next_task" ]; then
echo ""
log_info "Respawning worker in slot $slot"
global_worker_counter=$((global_worker_counter + 1))
spawn_worker "$slot" "$next_task" "$global_worker_counter"
echo "$next_task" > "$SLOT_DIR/slot_${slot}_task"
active_workers=$((active_workers + 1))
sleep 5
fi
done
fi
# Progress update every 30 seconds
if [ $((check_count % 6)) -eq 0 ]; then
local elapsed=$((check_count * check_interval))
local remaining=$(get_pending_count)
echo -e "${WHITE}[$(date +"%H:%M:%S")]${NC} ${CYAN}Progress:${NC} $tasks_completed completed, $tasks_failed failed, $remaining pending, $active_workers active"
fi
# Check if done
local remaining=$(get_pending_count)
local wip=$(get_wip_count)
if [ $active_workers -eq 0 ]; then
if [ "$STOP_SPAWNING" = true ]; then
log_info "Graceful shutdown complete"
break
elif [ "$remaining" -eq 0 ] && [ "$wip" -eq 0 ]; then
log_info "All tasks processed"
break
fi
fi
# Timeout
local elapsed_time=$((check_count * check_interval))
if [ $elapsed_time -ge $max_check_time ]; then
log_warning "Timeout reached"
break
fi
done
# Cleanup
rm -rf "$SLOT_DIR"
trap - SIGINT
# Final summary
local total_time=$((check_count * check_interval))
local minutes=$((total_time / 60))
local seconds=$((total_time % 60))
print_separator
echo ""
if [ "$STOP_SPAWNING" = true ]; then
echo -e "${YELLOW}${BOLD}GRACEFUL SHUTDOWN COMPLETE${NC}"
else
echo -e "${GREEN}${BOLD}ALL TASKS PROCESSED${NC}"
fi
print_separator
show_task_summary
echo -e " ${GREEN}Succeeded:${NC} $tasks_completed"
echo -e " ${RED}Failed:${NC} $tasks_failed"
echo -e " ${PURPLE}Workers Spawned:${NC} $global_worker_counter"
echo -e " ${BLUE}Log File:${NC} $LOG_FILE"
echo -e " ${YELLOW}Time:${NC} ${minutes}m ${seconds}s"
print_separator
verbose_say "Orchestrator finished. $tasks_completed succeeded, $tasks_failed failed."
log_success "Task orchestrator finished"
}
# ============================================
# CLI Argument Parsing
# ============================================
while [[ $# -gt 0 ]]; do
case $1 in
--tasks-dir|-t)
TASKS_DIR="$2"
shift 2
;;
--working-dir|-w)
WORKING_DIR="$2"
shift 2
;;
--max-parallel|-p)
MAX_PARALLEL="$2"
shift 2
;;
--verbose|-v)
VERBOSE=true
shift
;;
--init)
init_task_dirs
exit 0
;;
--status|-s)
print_banner
ensure_task_dirs
show_task_summary
exit 0
;;
--reset-failed|-r)
ensure_task_dirs
reset_failed
exit 0
;;
--reset-wip)
ensure_task_dirs
reset_wip
exit 0
;;
--help|-h)
echo -e "${CYAN}${BOLD}Task Orchestrator${NC}"
echo "Parallel Claude Code task runner"
echo ""
echo "Usage: $0 [options]"
echo ""
echo "Options:"
echo " -t, --tasks-dir PATH Tasks directory (default: ./tasks)"
echo " -w, --working-dir PATH Working directory for Claude (default: current)"
echo " -p, --max-parallel N Worker pool size (default: 10)"
echo " -v, --verbose Enable audio notifications"
echo " -s, --status Show task counts"
echo " -r, --reset-failed Move failed tasks back to pending"
echo " --reset-wip Move WIP tasks back to pending"
echo " --init Initialize task directories"
echo " -h, --help Show this help"
echo ""
echo "Directory Structure:"
echo " tasks/pending/ Tasks waiting to run"
echo " tasks/wip/ Currently running"
echo " tasks/completed/ Finished successfully"
echo " tasks/failed/ Failed tasks"
echo ""
echo "See tasks.MD for how to write task files."
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
echo "Use --help for usage"
exit 1
;;
esac
done
# Ensure directories exist before running
if [ ! -d "$TASKS_DIR/pending" ]; then
echo -e "${YELLOW}Task directories not found. Initializing...${NC}"
init_task_dirs
fi
# Run main
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment