workflows/default/systems/mcp/tools/task-create-bulk/script.ps1

function Invoke-TaskCreateBulk {
    param(
        [hashtable]$Arguments
    )
    
    # Extract arguments
    $tasks = $Arguments['tasks']
    
    # Validate required fields
    if (-not $tasks) {
        throw "Tasks array is required"
    }
    
    if ($tasks.Count -eq 0) {
        throw "At least one task must be provided"
    }
    
    # Validate categories and efforts
    # Read categories from settings.default.json if available; fall back to defaults
    $defaultCategories = @('core', 'feature', 'enhancement', 'bugfix', 'infrastructure', 'ui-ux')
    $settingsPath = Join-Path $global:DotbotProjectRoot ".bot\settings\settings.default.json"
    if (Test-Path $settingsPath) {
        $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
        if ($settings.task_categories) {
            $validCategories = @($settings.task_categories) + $defaultCategories | Select-Object -Unique
        } else {
            $validCategories = $defaultCategories
        }
    } else {
        $validCategories = $defaultCategories
    }
    $validEfforts = @('XS', 'S', 'M', 'L', 'XL')
    
    # Import task index module for dependency validation
    $indexModule = Join-Path $global:DotbotProjectRoot ".bot\systems\mcp\modules\TaskIndexCache.psm1"
    if (-not (Get-Module TaskIndexCache)) {
        Import-Module $indexModule -Force
    }
    
    # Initialize task index
    $tasksBaseDir = Join-Path $global:DotbotProjectRoot ".bot\workspace\tasks"
    Initialize-TaskIndex -TasksBaseDir $tasksBaseDir
    $index = Get-TaskIndex
    
    # Define tasks directory
    $tasksDir = Join-Path $global:DotbotProjectRoot ".bot\workspace\tasks\todo"
    
    # Ensure directory exists
    if (-not (Test-Path $tasksDir)) {
        New-Item -ItemType Directory -Force -Path $tasksDir | Out-Null
    }
    
    # Process each task
    $createdTasks = @()
    $errors = @()
    $basePriority = 1
    
    for ($i = 0; $i -lt $tasks.Count; $i++) {
        $task = $tasks[$i]
        
        try {
            # Validate required fields for this task
            if (-not $task.name) {
                throw "Task #$($i+1): name is required"
            }
            
            if (-not $task.description) {
                throw "Task #$($i+1): description is required"
            }
            
            # Validate category if provided
            if ($task.category -and $task.category -notin $validCategories) {
                throw "Task #$($i+1): Invalid category. Must be one of: $($validCategories -join ', ')"
            }
            
            # Validate effort if provided
            if ($task.effort -and $task.effort -notin $validEfforts) {
                throw "Task #$($i+1): Invalid effort. Must be one of: $($validEfforts -join ', ')"
            }
            
            # Set defaults
            $category = if ($task.category) { $task.category } else { 'feature' }
            $priority = if ($task.priority) { [int]$task.priority } else { $basePriority + $i }
            $effort = if ($task.effort) { $task.effort } else { 'M' }
            $dependencies = if ($task.dependencies -is [array]) {
                $task.dependencies
            } elseif ($task.dependencies -is [string]) {
                @($task.dependencies)
            } else {
                @()
            }
            $acceptanceCriteria = if ($task.acceptance_criteria) { $task.acceptance_criteria } else { @() }
            $steps = if ($task.steps) { $task.steps } else { @() }
            $applicableStandards = if ($task.applicable_standards) { $task.applicable_standards } else { @() }
            $applicableAgents = if ($task.applicable_agents) { $task.applicable_agents } else { @() }
            $applicableSkills = if ($task.applicable_skills) { $task.applicable_skills } else { @() }
            
            # Validate dependencies exist
            if ($dependencies -and $dependencies.Count -gt 0) {
                $invalidDeps = @()
                foreach ($dep in $dependencies) {
                    $depLower = $dep.ToLower()
                    $found = $false
                    
                    # Check all existing tasks
                    $allTasks = @($index.Todo.Values) + @($index.InProgress.Values) + @($index.Done.Values)
                    
                    # Also check previously created tasks in this batch
                    $allTasks += $createdTasks | ForEach-Object {
                        $taskSlug = ($_.name -replace '[^\w\s-]', '' -replace '\s+', '-').ToLower()
                        [PSCustomObject]@{
                            id = $_.id
                            name = $_.name
                            slug = $taskSlug
                        }
                    }
                    
                    foreach ($t in $allTasks) {
                        # Check ID match
                        if ($t.id -eq $dep) { $found = $true; break }
                        
                        # Check name match
                        if ($t.name -eq $dep) { $found = $true; break }
                        
                        # Check slug match
                        $taskSlug = if ($t.slug) { $t.slug } else { ($t.name -replace '[^\w\s-]', '' -replace '\s+', '-').ToLower() }
                        if ($taskSlug -eq $depLower) { $found = $true; break }
                        
                        # Fuzzy match
                        if ($taskSlug -like "*$depLower*" -or $depLower -like "*$taskSlug*") { $found = $true; break }
                    }
                    
                    if (-not $found) {
                        $invalidDeps += $dep
                    }
                }
                
                if ($invalidDeps.Count -gt 0) {
                    $depList = $invalidDeps -join "', '"
                    throw "Invalid dependencies: '$depList'. These tasks do not exist in the system or earlier in this batch."
                }
            }
            
            # Generate unique ID
            $id = [System.Guid]::NewGuid().ToString()
            
            # Create task object
            $newTask = @{
                id = $id
                name = $task.name
                description = $task.description
                category = $category
                priority = $priority
                effort = $effort
                status = 'todo'
                dependencies = $dependencies
                acceptance_criteria = $acceptanceCriteria
                steps = $steps
                applicable_standards = $applicableStandards
                applicable_agents = $applicableAgents
                applicable_skills = $applicableSkills
                needs_interview = ($task.needs_interview -eq $true)
                group_id = $task.group_id
                human_hours = $task.human_hours
                ai_hours = $task.ai_hours
                working_dir = $task.working_dir
                created_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
                updated_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
                completed_at = $null
            }

            # Passthrough: preserve extra/custom fields from input (e.g., research_prompt, external_repo)
            $reservedFields = @('id', 'status', 'created_at', 'updated_at', 'completed_at')
            # Use .Keys for dictionary entries; skip .NET internal properties from OrderedDictionary
            $dictKeys = if ($task -is [System.Collections.IDictionary]) { $task.Keys } else { $task.PSObject.Properties.Name }
            foreach ($key in $dictKeys) {
                if (-not $newTask.ContainsKey($key) -and $key -notin $reservedFields) {
                    $newTask[$key] = $task[$key]
                }
            }

            # Create filename from name (sanitized)
            $fileName = ($task.name -replace '[^\w\s-]', '' -replace '\s+', '-').ToLower()
            if ($fileName.Length -gt 50) {
                $fileName = $fileName.Substring(0, 50)
            }
            $fileName = "$fileName-$($id.Split('-')[0]).json"
            $filePath = Join-Path $tasksDir $fileName
            
            # Save task to file
            $newTask | ConvertTo-Json -Depth 10 | Set-Content -Path $filePath -Encoding UTF8
            
            # Add to created list
            $createdTasks += @{
                id = $id
                name = $task.name
                file_path = $filePath
                priority = $priority
            }
            
        } catch {
            $errors += @{
                index = $i
                name = $task.name
                error = $_.Exception.Message
            }
        }
    }
    
    # Return result
    return @{
        success = ($errors.Count -eq 0)
        created_count = $createdTasks.Count
        error_count = $errors.Count
        created_tasks = $createdTasks
        errors = $errors
        message = if ($errors.Count -eq 0) {
            "Successfully created $($createdTasks.Count) tasks"
        } else {
            "Created $($createdTasks.Count) tasks with $($errors.Count) errors"
        }
    }
}