workflows/default/systems/runtime/expand-task-groups.ps1

<#
.SYNOPSIS
Expands task groups into detailed tasks by invoking Claude once per group.

.DESCRIPTION
Phase 2b orchestrator. Reads task-groups.json, topologically sorts groups by
dependencies, then expands each group sequentially by invoking Claude with
the 03b-expand-task-group.md template. After all groups are expanded, generates
a roadmap-overview.md summary.

.PARAMETER BotRoot
Path to the .bot directory.

.PARAMETER Model
Claude model name to use (e.g., claude-sonnet-4-6).

.PARAMETER ProcessId
Process registry ID for activity logging.
#>


param(
    [Parameter(Mandatory)]
    [string]$BotRoot,

    # Settings object passed by Invoke-WorkflowProcess (contains execution.model etc.)
    $Settings,

    # Explicit model override — takes precedence over Settings.execution.model
    [string]$Model,

    [string]$ProcessId,

    # When set, look for prompt templates here first (workflow-scoped install)
    [string]$WorkflowDir
)

# Resolve model: explicit param > settings object > fallback
if (-not $Model) {
    if ($Settings -and $Settings.execution -and $Settings.execution.model) {
        $Model = $Settings.execution.model
    } else {
        $Model = 'claude-sonnet-4-6'
    }
}

# --- Setup ---
Import-Module "$BotRoot\systems\runtime\ClaudeCLI\ClaudeCLI.psm1" -Force
Import-Module "$BotRoot\systems\runtime\ProviderCLI\ProviderCLI.psm1" -Force
Import-Module "$BotRoot\systems\runtime\modules\DotBotTheme.psm1" -Force

$productDir = Join-Path $BotRoot "workspace\product"
$todoDir = Join-Path $BotRoot "workspace\tasks\todo"
# Resolve template: workflow-scoped install takes priority, fall back to global prompts dir
$templatePath = $null
if ($WorkflowDir) {
    $candidate = Join-Path $WorkflowDir "recipes\prompts\03b-expand-task-group.md"
    if (Test-Path $candidate) { $templatePath = $candidate }
}
if (-not $templatePath) {
    $templatePath = Join-Path $BotRoot "recipes\prompts\03b-expand-task-group.md"
}
$groupsPath = Join-Path $productDir "task-groups.json"

# Set process ID for activity logging
if ($ProcessId) {
    $env:DOTBOT_PROCESS_ID = $ProcessId
}

# --- Helpers ---

function Write-GroupActivity {
    param([string]$Message)
    try { Write-ActivityLog -Type "text" -Message $Message } catch { Write-BotLog -Level Debug -Message "Logging operation failed" -Exception $_ }
    Write-Status $Message -Type Info
}

function Get-TopologicalOrder {
    param([array]$Groups)

    $sorted = [System.Collections.ArrayList]::new()
    $remaining = [System.Collections.ArrayList]::new()
    foreach ($g in $Groups) { [void]$remaining.Add($g) }
    $resolvedIds = @{}

    $maxIterations = $Groups.Count + 1
    $iteration = 0

    while ($remaining.Count -gt 0) {
        $iteration++
        if ($iteration -gt $maxIterations) {
            throw "Circular dependency detected among groups: $(($remaining | ForEach-Object { $_.id }) -join ', ')"
        }

        $ready = @($remaining | Where-Object {
            $allMet = $true
            if ($_.depends_on) {
                foreach ($dep in $_.depends_on) {
                    if (-not $resolvedIds.ContainsKey($dep)) { $allMet = $false; break }
                }
            }
            $allMet
        })

        if ($ready.Count -eq 0) {
            throw "Circular dependency detected among groups: $(($remaining | ForEach-Object { $_.id }) -join ', ')"
        }

        # Sort ready items by order field for deterministic output
        $ready = $ready | Sort-Object { $_.order }

        foreach ($g in $ready) {
            [void]$sorted.Add($g)
            $resolvedIds[$g.id] = $true
            $remaining.Remove($g) | Out-Null
        }
    }

    return $sorted.ToArray()
}

# --- Main ---

# 1. Read task-groups.json
if (-not (Test-Path $groupsPath)) {
    throw "task-groups.json not found at: $groupsPath"
}

$manifest = Get-Content $groupsPath -Raw | ConvertFrom-Json
$groups = @($manifest.groups)

Write-Header "Task Group Expansion"
Write-GroupActivity "Expanding $($groups.Count) task groups into detailed tasks"

# 2. Read template
if (-not (Test-Path $templatePath)) {
    throw "Template not found: $templatePath"
}
$template = Get-Content $templatePath -Raw

# 3. Topological sort
$sortedGroups = Get-TopologicalOrder -Groups $groups
Write-GroupActivity "Expansion order: $(($sortedGroups | ForEach-Object { $_.name }) -join ' -> ')"

# 4. Expand each group
$groupTaskMap = @{}  # group_id -> array of {id, name}
$totalTasksCreated = 0

foreach ($group in $sortedGroups) {
    Write-Header "Group: $($group.name)"
    Write-GroupActivity "Expanding group: $($group.name) (order $($group.order))"

    # Build dependency task list from prerequisite groups
    $depTasks = @()
    if ($group.depends_on) {
        foreach ($depGroupId in $group.depends_on) {
            if ($groupTaskMap.ContainsKey($depGroupId)) {
                $depTasks += $groupTaskMap[$depGroupId]
            }
        }
    }

    $depTasksJson = if ($depTasks.Count -gt 0) {
        "Tasks from prerequisite groups:`n``````json`n$($depTasks | ConvertTo-Json -Depth 5)`n```````n`nYou may reference these task IDs in the ``dependencies`` array where technically justified."
    } else {
        "No prerequisite tasks. This is a root group with no cross-group dependencies."
    }

    # Build scope list
    $scopeList = if ($group.scope) {
        ($group.scope | ForEach-Object { "- $_" }) -join "`n"
    } else {
        "- (No specific scope items defined)"
    }

    # Build acceptance criteria list
    $acList = if ($group.acceptance_criteria) {
        ($group.acceptance_criteria | ForEach-Object { "- $_" }) -join "`n"
    } else {
        "- (No specific acceptance criteria defined)"
    }

    # Extract priority range
    $priorityMin = if ($group.priority_range -and $group.priority_range.Count -ge 2) { $group.priority_range[0] } else { 1 }
    $priorityMax = if ($group.priority_range -and $group.priority_range.Count -ge 2) { $group.priority_range[1] } else { 100 }

    # Substitute template variables
    $prompt = $template
    $prompt = $prompt -replace '\{\{GROUP_ID\}\}', $group.id
    $prompt = $prompt -replace '\{\{GROUP_NAME\}\}', $group.name
    $prompt = $prompt -replace '\{\{GROUP_DESCRIPTION\}\}', $group.description
    $prompt = $prompt -replace '\{\{GROUP_SCOPE\}\}', $scopeList
    $prompt = $prompt -replace '\{\{GROUP_ACCEPTANCE_CRITERIA\}\}', $acList
    $prompt = $prompt -replace '\{\{PRIORITY_MIN\}\}', $priorityMin
    $prompt = $prompt -replace '\{\{PRIORITY_MAX\}\}', $priorityMax
    $prompt = $prompt -replace '\{\{CATEGORY_HINT\}\}', $group.category_hint
    $prompt = $prompt -replace '\{\{DEPENDENCY_TASKS\}\}', $depTasksJson

    # Snapshot todo directory before expansion
    $beforeFiles = @()
    if (Test-Path $todoDir) {
        $beforeFiles = @(Get-ChildItem -Path $todoDir -Filter "*.json" | ForEach-Object { $_.FullName })
    }

    # Invoke provider to expand this group
    $sessionId = New-ProviderSession
    try {
        Invoke-ProviderStream -Prompt $prompt -Model $Model -SessionId $sessionId -PersistSession:$false
    } catch {
        Write-GroupActivity "Error expanding group $($group.name): $($_.Exception.Message)"
        Write-Status "Failed to expand group: $($group.name)" -Type Error
        continue
    }

    # Discover newly created tasks
    $afterFiles = @()
    if (Test-Path $todoDir) {
        $afterFiles = @(Get-ChildItem -Path $todoDir -Filter "*.json" | ForEach-Object { $_.FullName })
    }
    $newFiles = @($afterFiles | Where-Object { $_ -notin $beforeFiles })

    $newTasks = @()
    foreach ($f in $newFiles) {
        try {
            $taskData = Get-Content $f -Raw | ConvertFrom-Json
            $newTasks += @{ id = $taskData.id; name = $taskData.name }
        } catch { Write-BotLog -Level Debug -Message "Failed to parse data" -Exception $_ }
    }

    $groupTaskMap[$group.id] = $newTasks
    $totalTasksCreated += $newTasks.Count

    Write-GroupActivity "Group '$($group.name)' expanded: $($newTasks.Count) tasks created"

    # Brief pause between groups to avoid rate limits
    if ($group -ne $sortedGroups[-1]) {
        Start-Sleep -Seconds 2
    }
}

# 5. Append expansion results to roadmap-overview.md (generated in Phase 2a)
Write-GroupActivity "Appending expansion results to roadmap overview..."

$overviewPath = Join-Path $productDir "roadmap-overview.md"
$appendLines = [System.Collections.ArrayList]::new()
[void]$appendLines.Add("")
[void]$appendLines.Add("---")
[void]$appendLines.Add("")
[void]$appendLines.Add("## Expansion Results")
[void]$appendLines.Add("")
[void]$appendLines.Add("**Total tasks created:** $totalTasksCreated")
[void]$appendLines.Add("")

foreach ($group in $sortedGroups) {
    $taskCount = if ($groupTaskMap.ContainsKey($group.id)) { $groupTaskMap[$group.id].Count } else { 0 }
    [void]$appendLines.Add("### $($group.order). $($group.name) ($taskCount tasks)")
    [void]$appendLines.Add("")

    if ($groupTaskMap.ContainsKey($group.id)) {
        foreach ($task in $groupTaskMap[$group.id]) {
            [void]$appendLines.Add(" - $($task.name)")
        }
        [void]$appendLines.Add("")
    }
}

[void]$appendLines.Add("## Next Steps")
[void]$appendLines.Add("")
[void]$appendLines.Add("1. Review task list and adjust priorities if needed")
[void]$appendLines.Add("2. Begin implementation with ``task_get_next``")
[void]$appendLines.Add("3. Run analysis loop to prepare tasks for execution")
[void]$appendLines.Add("")

if (Test-Path $overviewPath) {
    $appendLines -join "`n" | Add-Content -Path $overviewPath -Encoding UTF8
} else {
    # Fallback if Phase 2a roadmap wasn't generated
    $appendLines -join "`n" | Set-Content -Path $overviewPath -Encoding UTF8
}
Write-GroupActivity "Expansion results appended to: $overviewPath"

# Final summary
Write-Header "Expansion Complete"
Write-GroupActivity "Task group expansion complete: $totalTasksCreated tasks created across $($sortedGroups.Count) groups"

# Emit a structured phase-completion marker so UI/state code can latch on
# without parsing the free-text Write-GroupActivity message above.
try {
    Write-ActivityLog -Type "phase_complete" -Message "phase=task-group-expansion tasks_created=$totalTasksCreated groups=$($sortedGroups.Count)"
} catch { Write-BotLog -Level Debug -Message "phase_complete marker write failed" -Exception $_ }