extensions/specrew-speckit/scripts/deploy-squad-runtime.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$ProjectPath,

    [switch]$DryRun,
    [switch]$PassThru
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Add-DeploymentAction {
    param(
        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions,

        [Parameter(Mandatory = $true)]
        [string]$Action,

        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $null = $Actions.Add([pscustomobject]@{
            Action = $Action
            Path   = $Path
        })
}

function Ensure-Directory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (Test-Path -LiteralPath $Path) {
        Add-DeploymentAction -Actions $Actions -Action 'preserved-directory' -Path $Path
        return
    }

    Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-create-directory' } else { 'created-directory' }) -Path $Path
    if (-not $DryRun) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
}

function Write-MissingFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [Parameter(Mandatory = $true)]
        [string]$Content,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (Test-Path -LiteralPath $TargetPath) {
        Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
        return
    }

    Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-create' } else { 'created' }) -Path $TargetPath
    if (-not $DryRun) {
        $parent = Split-Path -Parent $TargetPath
        if (-not (Test-Path -LiteralPath $parent)) {
            New-Item -ItemType Directory -Path $parent -Force | Out-Null
        }

        [System.IO.File]::WriteAllText($TargetPath, $Content, [System.Text.UTF8Encoding]::new($false))
    }
}

function Set-ManagedFile {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [Parameter(Mandatory = $true)]
        [string]$Content,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (-not (Test-Path -LiteralPath $TargetPath)) {
        Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-create' } else { 'created' }) -Path $TargetPath
        if (-not $DryRun) {
            $parent = Split-Path -Parent $TargetPath
            if (-not (Test-Path -LiteralPath $parent)) {
                New-Item -ItemType Directory -Path $parent -Force | Out-Null
            }

            [System.IO.File]::WriteAllText($TargetPath, $Content, [System.Text.UTF8Encoding]::new($false))
        }

        return
    }

    $existingContent = Get-Content -LiteralPath $TargetPath -Raw
    if ($existingContent -eq $Content) {
        Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
        return
    }

    Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-update' } else { 'updated' }) -Path $TargetPath
    if (-not $DryRun) {
        [System.IO.File]::WriteAllText($TargetPath, $Content, [System.Text.UTF8Encoding]::new($false))
    }
}

function Get-ManagedBlock {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [string]$Content
    )

    return @(
        "<!-- >>> specrew-managed $Name >>> -->"
        $Content.Trim()
        "<!-- <<< specrew-managed $Name <<< -->"
    ) -join [Environment]::NewLine
}

function Remove-LegacyManagedContent {
    param(
        [Parameter(Mandatory = $true)]
        [string]$BlockName,

        [Parameter(Mandatory = $true)]
        [string]$ExistingContent
    )

    $updatedContent = $ExistingContent
    $migrated = $false

    switch ($BlockName) {
        'ceremonies' {
            $legacyPattern = '(?ms)\s*<!-- specrew:ceremony:[^>]+:start -->.*?<!-- specrew:ceremony:[^>]+:end -->\s*'
            $replacement = [regex]::Replace($updatedContent, $legacyPattern, [Environment]::NewLine + [Environment]::NewLine)
            if ($replacement -ne $updatedContent) {
                $updatedContent = $replacement
                $migrated = $true
            }
        }
        'directives' {
            $legacyPattern = '(?ms)\s*## Specrew Directives\s*(?:\r?\n)+(?:<!-- specrew:directive:[^>]+:start -->.*?<!-- specrew:directive:[^>]+:end -->\s*)+'
            $replacement = [regex]::Replace($updatedContent, $legacyPattern, [Environment]::NewLine + [Environment]::NewLine)
            if ($replacement -ne $updatedContent) {
                $updatedContent = $replacement
                $migrated = $true
            }
        }
        'baseline-roles' {
            $legacyRows = @(
                'Spec Steward',
                'Planner',
                'Implementer',
                'Reviewer',
                'Retro Facilitator'
            ) | ForEach-Object { [regex]::Escape($_) }
            $legacyRowPattern = '(?m)^\|\s*[^|]+\s*\|\s*(?:' + ($legacyRows -join '|') + ')\s*\|.*\r?\n?'
            $replacement = [regex]::Replace($updatedContent, $legacyRowPattern, '')
            if ($replacement -ne $updatedContent) {
                $updatedContent = $replacement
                $migrated = $true
            }
        }
    }

    if ($migrated) {
        $updatedContent = [regex]::Replace($updatedContent, '(?m)(\r?\n){3,}', [Environment]::NewLine + [Environment]::NewLine)
        $updatedContent = $updatedContent.TrimEnd()
        if (-not [string]::IsNullOrWhiteSpace($updatedContent)) {
            $updatedContent += [Environment]::NewLine
        }
    }

    return [pscustomobject]@{
        Migrated = $migrated
        Content  = $updatedContent
    }
}

function Set-ManagedBlock {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [Parameter(Mandatory = $true)]
        [string]$BlockName,

        [Parameter(Mandatory = $true)]
        [string]$ManagedContent,

        [string]$BaseContentIfMissing = '',

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    $managedBlock = Get-ManagedBlock -Name $BlockName -Content $ManagedContent
    $startMarker = [regex]::Escape("<!-- >>> specrew-managed $BlockName >>> -->")
    $endMarker = [regex]::Escape("<!-- <<< specrew-managed $BlockName <<< -->")
    $managedPattern = "(?ms)\s*$startMarker.*?$endMarker\s*"

    if (-not (Test-Path -LiteralPath $TargetPath)) {
        Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-create' } else { 'created' }) -Path $TargetPath
        if (-not $DryRun) {
            $parent = Split-Path -Parent $TargetPath
            if (-not (Test-Path -LiteralPath $parent)) {
                New-Item -ItemType Directory -Path $parent -Force | Out-Null
            }

            $parts = @()
            if (-not [string]::IsNullOrWhiteSpace($BaseContentIfMissing)) {
                $parts += $BaseContentIfMissing.TrimEnd()
            }

            $parts += $managedBlock
            $content = ($parts -join ([Environment]::NewLine + [Environment]::NewLine)) + [Environment]::NewLine
            [System.IO.File]::WriteAllText($TargetPath, $content, [System.Text.UTF8Encoding]::new($false))
        }

        return
    }

    $existingContent = Get-Content -LiteralPath $TargetPath -Raw
    $legacyMigration = Remove-LegacyManagedContent -BlockName $BlockName -ExistingContent $existingContent
    if ($legacyMigration.Migrated) {
        $existingContent = $legacyMigration.Content
    }

    if ($existingContent -match $managedPattern) {
        $updatedContent = [regex]::Replace($existingContent, $managedPattern, ([Environment]::NewLine + [Environment]::NewLine + $managedBlock + [Environment]::NewLine + [Environment]::NewLine))
    }
    else {
        $trimmedExistingContent = $existingContent.TrimEnd()
        if ([string]::IsNullOrWhiteSpace($trimmedExistingContent)) {
            $updatedContent = $managedBlock
        }
        else {
            $updatedContent = $trimmedExistingContent + [Environment]::NewLine + [Environment]::NewLine + $managedBlock
        }
    }

    $updatedContent = $updatedContent.TrimEnd() + [Environment]::NewLine
    if ($updatedContent -eq $existingContent) {
        Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
        return
    }

    Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-update' } else { 'updated' }) -Path $TargetPath
    if (-not $DryRun) {
        [System.IO.File]::WriteAllText($TargetPath, $updatedContent, [System.Text.UTF8Encoding]::new($false))
    }
}

function Set-ManagedTableRows {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [Parameter(Mandatory = $true)]
        [string]$TableSectionHeader,

        [Parameter(Mandatory = $true)]
        [string[]]$Rows,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (-not (Test-Path -LiteralPath $TargetPath)) {
        return
    }

    $existingContent = Get-Content -LiteralPath $TargetPath -Raw
    
    # Find the markdown table separator line (like |------|------|) after the section header
    # and insert rows immediately after it
    $escapedHeader = [regex]::Escape($TableSectionHeader)
    # Match: section header, followed by anything, then a table header row, then a separator row
    # Capture group 1: everything up to and including the separator line
    $tablePattern = "($escapedHeader[^\r\n]*\r?\n(?:.*?\r?\n)*?\|[^\r\n]+\|\r?\n\|[\s\-|]+\|\r?\n)"
    
    if ($existingContent -match $tablePattern) {
        $rowsContent = ($Rows | ForEach-Object { $_ + [Environment]::NewLine }) -join ''
        $updatedContent = $existingContent -replace $tablePattern, ('${1}' + $rowsContent)
        
        if ($updatedContent -ne $existingContent) {
            Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-update' } else { 'updated' }) -Path $TargetPath
            if (-not $DryRun) {
                [System.IO.File]::WriteAllText($TargetPath, $updatedContent, [System.Text.UTF8Encoding]::new($false))
            }
        }
        else {
            Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
        }
    }
    else {
        Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
    }
}

function Get-DirectiveDeployment {
    param(
        [Parameter(Mandatory = $true)]
        [string]$DirectivePath
    )

    $content = Get-Content -LiteralPath $DirectivePath -Raw
    $directiveTitlePattern = [regex]::new('^\s*#\s*Directive:\s*', [System.Text.RegularExpressions.RegexOptions]::Multiline)
    $content = $directiveTitlePattern.Replace($content, '## ', 1)
    $content = [regex]::Replace($content, '(?ms)\r?\n---\r?\n\r?\n\*\*Deployment\*\*:.*$', '')
    return $content.Trim()
}

function Get-BaselineRoleDefinitions {
    return @(
        [pscustomobject]@{
            Name           = 'Spec Steward'
            AgentDirectory = 'spec-steward'
            TemplatePath   = 'agents/spec-steward/charter.md'
            DirectivePaths = @('directives/spec-authority.md')
        }
        [pscustomobject]@{
            Name           = 'Planner'
            AgentDirectory = 'planner'
            TemplatePath   = 'agents/planner/charter.md'
            DirectivePaths = @('directives/spec-authority.md', 'directives/traceability.md')
        }
        [pscustomobject]@{
            Name           = 'Implementer'
            AgentDirectory = 'implementer'
            TemplatePath   = 'agents/implementer/charter.md'
            DirectivePaths = @('directives/spec-authority.md', 'directives/drift-reporting.md')
        }
        [pscustomobject]@{
            Name           = 'Reviewer'
            AgentDirectory = 'reviewer'
            TemplatePath   = 'agents/reviewer/charter.md'
            DirectivePaths = @('directives/spec-authority.md', 'directives/drift-reporting.md')
        }
        [pscustomobject]@{
            Name           = 'Retro Facilitator'
            AgentDirectory = 'retro-facilitator'
            TemplatePath   = 'agents/retro-facilitator/charter.md'
            DirectivePaths = @('directives/spec-authority.md')
        }
    )
}

$resolvedProjectPath = Resolve-ProjectPath -Path $ProjectPath
$extensionRoot = Split-Path -Parent $PSScriptRoot
$templateRoot = Join-Path $extensionRoot 'squad-templates'
$copilotSkillsRoot = Join-Path $resolvedProjectPath '.copilot\skills'
$squadRoot = Join-Path $resolvedProjectPath '.squad'
$squadAgentsRoot = Join-Path $squadRoot 'agents'
$coordinatorPromptPath = Join-Path $resolvedProjectPath '.github\agents\squad.agent.md'
$ceremoniesPath = Join-Path $squadRoot 'ceremonies.md'
$teamPath = Join-Path $squadRoot 'team.md'
$actions = [System.Collections.ArrayList]::new()

if (-not (Test-Path -LiteralPath $squadRoot) -and -not $DryRun) {
    throw "Squad must be initialized before deploying runtime surfaces. Missing '$squadRoot'."
}

if ($DryRun -and -not (Test-Path -LiteralPath $squadRoot)) {
    Add-DeploymentAction -Actions $actions -Action 'would-create-directory' -Path $squadRoot
}

Ensure-Directory -Path $copilotSkillsRoot -Actions $actions
Ensure-Directory -Path $squadAgentsRoot -Actions $actions
Ensure-Directory -Path (Join-Path $squadRoot 'casting') -Actions $actions

$skillsTemplateRoot = Join-Path $templateRoot 'skills'
$skillFiles = @(Get-ChildItem -LiteralPath $skillsTemplateRoot -Filter '*.md' | Where-Object { $_.Name -ne 'README.md' } | Sort-Object Name)
foreach ($skillFile in $skillFiles) {
    $skillName = 'specrew-{0}' -f $skillFile.BaseName
    $skillDir = Join-Path $copilotSkillsRoot $skillName
    Ensure-Directory -Path $skillDir -Actions $actions
    Set-ManagedFile -TargetPath (Join-Path $skillDir 'SKILL.md') -Content (Get-Content -LiteralPath $skillFile.FullName -Raw) -Actions $actions
}

# Subdirectory-style skills (for example the slash-command runtime surfaces) deploy as-is.
$skillDirectories = @(Get-ChildItem -LiteralPath $skillsTemplateRoot -Directory | Sort-Object Name)
foreach ($skillDirectory in $skillDirectories) {
    $skillSourcePath = Join-Path $skillDirectory.FullName 'SKILL.md'
    if (-not (Test-Path -LiteralPath $skillSourcePath -PathType Leaf)) {
        continue
    }

    $skillDir = Join-Path $copilotSkillsRoot $skillDirectory.Name
    Ensure-Directory -Path $skillDir -Actions $actions
    Set-ManagedFile -TargetPath (Join-Path $skillDir 'SKILL.md') -Content (Get-Content -LiteralPath $skillSourcePath -Raw) -Actions $actions
}

$coordinatorGovernancePath = Join-Path $templateRoot 'coordinator\specrew-governance.md'
if (-not (Test-Path -LiteralPath $coordinatorGovernancePath -PathType Leaf)) {
    throw "Missing coordinator governance template: $coordinatorGovernancePath"
}

if (Test-Path -LiteralPath $coordinatorPromptPath -PathType Leaf) {
    $coordinatorGovernanceContent = Get-Content -LiteralPath $coordinatorGovernancePath -Raw
    Set-ManagedBlock -TargetPath $coordinatorPromptPath -BlockName 'specrew-governance' -ManagedContent $coordinatorGovernanceContent -Actions $actions
}
else {
    Add-DeploymentAction -Actions $actions -Action 'skipped' -Path $coordinatorPromptPath
}

$ceremonyContent = (@(
        foreach ($ceremonyPath in @(
                (Join-Path $templateRoot 'ceremonies\planning.md'),
                (Join-Path $templateRoot 'ceremonies\review-demo.md')
            )) {
            (Get-Content -LiteralPath $ceremonyPath -Raw).Trim()
        }
    ) -join ([Environment]::NewLine + [Environment]::NewLine + '---' + [Environment]::NewLine + [Environment]::NewLine))
Set-ManagedBlock -TargetPath $ceremoniesPath -BlockName 'ceremonies' -ManagedContent $ceremonyContent -BaseContentIfMissing '# Ceremonies' -Actions $actions

$baselineRoles = @(Get-BaselineRoleDefinitions)

# Add explicit team status metadata to signal Squad readiness
$teamStatusBlock = @"
**Team Status**: configured
**Baseline Roles**: Spec Steward, Planner, Implementer, Reviewer, Retro Facilitator
**Configuration**: Specrew-managed baseline
"@

Set-ManagedBlock -TargetPath $teamPath -BlockName 'team-status' -ManagedContent $teamStatusBlock -BaseContentIfMissing '# Squad Team' -Actions $actions

# Update team.md Members table with baseline roles
$membersTableRows = @()
foreach ($baselineRole in $baselineRoles) {
    $membersTableRows += ('| {0} | {1} | `.squad/agents/{2}/charter.md` | baseline |' -f $baselineRole.AgentDirectory, $baselineRole.Name, $baselineRole.AgentDirectory)
}
if ($membersTableRows.Count -gt 0) {
    Set-ManagedTableRows -TargetPath $teamPath -TableSectionHeader '## Members' -Rows $membersTableRows -Actions $actions
}

# Also maintain the Specrew Baseline Roles section for documentation
$teamContentLines = @(
    '## Specrew Baseline Roles'
    ''
    '| Role | Charter | Status |'
    '| ---- | ------- | ------ |'
)
foreach ($baselineRole in $baselineRoles) {
    $teamContentLines += ('| {0} | `.squad/agents/{1}/charter.md` | baseline |' -f $baselineRole.Name, $baselineRole.AgentDirectory)
}
Set-ManagedBlock -TargetPath $teamPath -BlockName 'baseline-roles' -ManagedContent ($teamContentLines -join [Environment]::NewLine) -BaseContentIfMissing '# Squad Team' -Actions $actions

# Update routing.md with baseline role routing
$routingPath = Join-Path $squadRoot 'routing.md'
$routingTableRows = @(
    '| Specification governance | spec-steward | Spec authoring, requirement authority, drift detection |'
    '| Planning & traceability | planner | Iteration planning, task breakdown, requirement tracing |'
    '| Implementation | implementer | Code changes, feature delivery, execution follow-through |'
    '| Code review | reviewer | PR review, quality checks, acceptance validation |'
    '| Retrospectives | retro-facilitator | Iteration retrospectives, process improvements |'
)
Set-ManagedTableRows -TargetPath $routingPath -TableSectionHeader '## Routing Table' -Rows $routingTableRows -Actions $actions

# Update casting/registry.json with baseline role entries
$registryPath = Join-Path $squadRoot 'casting\registry.json'
$registryAgents = [ordered]@{}
foreach ($baselineRole in $baselineRoles) {
    $registryAgents[$baselineRole.AgentDirectory] = @{
        name = $baselineRole.Name
        role = $baselineRole.Name
        status = 'baseline'
        charter = ".squad/agents/$($baselineRole.AgentDirectory)/charter.md"
    }
}
$registryContent = @{
    agents = $registryAgents
} | ConvertTo-Json -Depth 10
Set-ManagedFile -TargetPath $registryPath -Content $registryContent -Actions $actions

foreach ($baselineRole in $baselineRoles) {
    $agentDirectory = Join-Path $squadAgentsRoot $baselineRole.AgentDirectory
    Ensure-Directory -Path $agentDirectory -Actions $actions

    $charterTemplate = Get-Content -LiteralPath (Join-Path $templateRoot $baselineRole.TemplatePath) -Raw
    $directiveContent = @(
        foreach ($directivePath in $baselineRole.DirectivePaths) {
            Get-DirectiveDeployment -DirectivePath (Join-Path $templateRoot $directivePath)
        }
    ) -join ([Environment]::NewLine + [Environment]::NewLine)

    Set-ManagedBlock -TargetPath (Join-Path $agentDirectory 'charter.md') -BlockName 'directives' -ManagedContent $directiveContent -BaseContentIfMissing $charterTemplate -Actions $actions

    # Create history.md for each baseline role
    $historyPath = Join-Path $agentDirectory 'history.md'
    $historyContent = @"
# $($baselineRole.Name) History
 
Project-specific learnings and patterns discovered during work.
 
## Patterns
 
<!-- Append entries below. Format: **Pattern:** description. **Context:** when it applies. -->
"@

    Write-MissingFile -TargetPath $historyPath -Content $historyContent -Actions $actions
}

if ($PassThru) {
    $actions
    return
}

$actions | Select-Object Action, Path | Format-Table -AutoSize
Write-Host ("Squad runtime deployment {0} for {1}" -f ($(if ($DryRun) { 'previewed' } else { 'completed' }), $resolvedProjectPath)) -ForegroundColor Green
exit 0