scripts/internal/coordinator-prompt-surgery.ps1

# Coordinator-prompt surgery — registry-driven rules engine (Phase C.3 refactor)
#
# Originally a 123-line file with hardcoded per-host switches (FR-011 header,
# FR-012 strip non-Copilot, FR-014 Codex pwsh-form). Now a thin rules engine
# that loads hosts/<kind>/coordinator-rules.psd1 and applies declared Rules in order.
#
# The universal header rewrite (FR-011) stays here as a built-in baseline because
# the literal IS the same across all hosts (it's the spec invariant). Per-host
# rule files declare ADDITIONAL surgery on top.
#
# To change a per-host rule: edit hosts/<kind>/coordinator-rules.psd1.
# To add a new host: create hosts/<kind>/coordinator-rules.psd1 — no edits to this engine.

Set-StrictMode -Version Latest

$script:RegistryPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'hosts\_registry.ps1'
if (-not (Test-Path -LiteralPath $script:RegistryPath -PathType Leaf)) {
    # Module-mode lookup
    $script:RegistryPath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) 'hosts\_registry.ps1'
}
if (-not (Test-Path -LiteralPath $script:RegistryPath -PathType Leaf)) {
    throw "Host registry not found. Searched: $script:RegistryPath"
}
. $script:RegistryPath

$script:CoordinatorRulesCache = @{}

function Get-SpecrewUniversalCoordinatorHeader {
    # FR-011 invariant: same literal for every host.
    return 'You are the Crew team coordinator running inside a Specrew-bootstrapped repository.'
}

function Get-SpecrewOriginalCoordinatorHeaderPattern {
    # Matches the original Squad header that gets replaced uniformly.
    return '(?m)^You are Squad running inside a Specrew-bootstrapped repository\.'
}

function Get-SpecrewHostOrientationMarker {
    return '<<SPECREW_HOST_ORIENTATION_BLOCK>>'
}

function Get-SpecrewHostOrientationBlock {
    <#
    .SYNOPSIS
    Renders the visible first-output orientation block for the selected launch host.
 
    .DESCRIPTION
    The shared start prompt owns lifecycle requirements only. Host-facing identity
    text is rendered here from the selected host manifest plus the runtime status
    recorded into start-context.json, so the visible orientation cannot drift into
    a stale hard-coded host/runtime claim.
    #>

    param(
        [Parameter(Mandatory = $true)]
        [ValidateSet('copilot', 'claude', 'codex', 'antigravity', 'cursor')]
        [string]$HostKind,

        [string]$CrewRuntimeStatus = 'bootstrap_only'
    )

    $manifest = Get-HostManifest -Kind $HostKind
    $displayName = if ($manifest.ContainsKey('DisplayName') -and -not [string]::IsNullOrWhiteSpace([string]$manifest.DisplayName)) {
        [string]$manifest.DisplayName
    }
    else {
        $HostKind
    }

    $hasSquadRuntime = ([string]$CrewRuntimeStatus -eq 'squad-runtime')
    $runtimeName = if ($manifest.ContainsKey('CrewRuntimeDisplayName') -and -not [string]::IsNullOrWhiteSpace([string]$manifest.CrewRuntimeDisplayName)) {
        [string]$manifest.CrewRuntimeDisplayName
    }
    else {
        'Crew role runtime'
    }

    $howThisWorks = if ($hasSquadRuntime) {
        "How this works: Specrew governs the spec -> plan -> implement -> review -> retro`nlifecycle. The $runtimeName runtime coordinates the Spec Steward, Planner,`nImplementer, Reviewer, and Retro Facilitator roles for this session."
    }
    else {
        "How this works: Specrew governs the spec -> plan -> implement -> review -> retro`nlifecycle. This $displayName session follows the saved lifecycle prompt and`nstructured start context directly; a separate role runtime is not active for this launch."
    }

    return @"
``````markdown
Welcome — I'm your Specrew Crew coordinator (running on $displayName).
 
$howThisWorks
 
What I'll ask from you: clarify questions when something is genuinely ambiguous
(2-3 max per phase), and an approve/redirect verdict at each boundary stop. I'll
emit a clear human re-entry packet every time I need you.
 
What you can browse: artifacts land under file:///<project-root-url>/specs/<feature>/ — spec file file:///<project-root-url>/specs/<feature>/spec.md, plan file file:///<project-root-url>/specs/<feature>/plan.md, tasks file file:///<project-root-url>/specs/<feature>/tasks.md, plus the iteration artifacts under file:///<project-root-url>/specs/<feature>/iterations/001/. Open another terminal and run ``code .`` to browse them while I work. After each iteration close, your dashboard lives at file:///<project-root-url>/specs/<feature>/iterations/<NNN>/dashboard.md.
 
Starting now: <one specific action — e.g. "creating feature 001-tip-calculator
and drafting the spec">.
``````
"@

}

function Get-SpecrewHostCoordinatorRules {
    <#
    .SYNOPSIS
    Loads the declarative coordinator-prompt surgery rules for a given host.
    .OUTPUTS
    array of hashtables, each with @{ Kind = 'Strip'|'Replace'; Pattern; Replacement?; Description }
    Returns empty array if the host has no per-host rules (e.g., Copilot).
    #>

    param([Parameter(Mandatory = $true)][string]$HostKind)

    $kindLower = $HostKind.ToLowerInvariant()
    if ($script:CoordinatorRulesCache.ContainsKey($kindLower)) {
        return $script:CoordinatorRulesCache[$kindLower]
    }

    $manifest = Get-HostManifest -Kind $kindLower
    $rulesFile = if ($manifest.ContainsKey('CoordinatorRulesFile') -and -not [string]::IsNullOrWhiteSpace([string]$manifest.CoordinatorRulesFile)) { $manifest.CoordinatorRulesFile } else { 'coordinator-rules.psd1' }
    $hostsRoot = Get-SpecrewHostsRoot
    $rulesPath = Join-Path (Join-Path $hostsRoot $kindLower) $rulesFile

    if (-not (Test-Path -LiteralPath $rulesPath -PathType Leaf)) {
        # Hosts may legitimately have no per-host rules (e.g., Copilot only needs the engine's universal header)
        $script:CoordinatorRulesCache[$kindLower] = @()
        return @()
    }

    try {
        $rulesData = Import-PowerShellDataFile -LiteralPath $rulesPath
    }
    catch {
        throw "Failed to load coordinator rules for host '$HostKind' at '$rulesPath': $($_.Exception.Message)"
    }

    if (-not $rulesData.ContainsKey('Rules')) {
        $script:CoordinatorRulesCache[$kindLower] = @()
        return @()
    }

    $rules = @($rulesData.Rules)
    $script:CoordinatorRulesCache[$kindLower] = $rules
    return $rules
}

function Invoke-SpecrewCoordinatorPromptSurgery {
    <#
    .SYNOPSIS
    Applies multi-host coordinator-prompt surgery — registry-driven rules engine.
 
    .DESCRIPTION
    Two surgeries applied in order:
      1. Universal header rewrite (FR-011 invariant; built-in baseline applied to ALL hosts).
      2. Per-host declarative rules from hosts/<kind>/coordinator-rules.psd1 applied in declared order.
 
    Per-host rules are hashtables with:
      - Kind = 'Strip' | 'Replace'
      - Pattern = regex string
      - Replacement = string (required only for Replace; supports regex backreferences like `$1`)
      - Description = human-readable label (for diagnostics)
 
    Returns the rewritten prompt body.
    #>

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

        [Parameter(Mandatory = $true)]
        [ValidateSet('copilot', 'claude', 'codex', 'antigravity', 'cursor')]
        [string]$HostKind,

        [string]$CrewRuntimeStatus = 'bootstrap_only'
    )

    if ([string]::IsNullOrEmpty($Prompt)) {
        return $Prompt
    }

    $result = $Prompt

    # Surgery 1: universal header rewrite (FR-011) — applies to ALL hosts as a built-in baseline.
    $result = [regex]::Replace($result, (Get-SpecrewOriginalCoordinatorHeaderPattern), (Get-SpecrewUniversalCoordinatorHeader))

    # Surgery 1b: host-facing orientation block rendered from the selected host package.
    $orientationMarker = [regex]::Escape((Get-SpecrewHostOrientationMarker))
    $orientationBlock = Get-SpecrewHostOrientationBlock -HostKind $HostKind -CrewRuntimeStatus $CrewRuntimeStatus
    $result = [regex]::Replace($result, $orientationMarker, [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $orientationBlock })

    # Surgery 2: per-host declarative rules
    $rules = Get-SpecrewHostCoordinatorRules -HostKind $HostKind
    $appliedStrip = $false
    foreach ($rule in $rules) {
        if (-not $rule.ContainsKey('Kind') -or -not $rule.ContainsKey('Pattern')) {
            Write-Warning ("Skipping malformed coordinator rule for host '{0}': missing Kind or Pattern" -f $HostKind)
            continue
        }
        switch ($rule.Kind) {
            'Strip' {
                $result = [regex]::Replace($result, $rule.Pattern, '')
                $appliedStrip = $true
            }
            'Replace' {
                if (-not $rule.ContainsKey('Replacement')) {
                    Write-Warning ("Skipping malformed Replace rule for host '{0}': missing Replacement" -f $HostKind)
                    continue
                }
                $result = [regex]::Replace($result, $rule.Pattern, [string]$rule.Replacement)
            }
            default {
                Write-Warning ("Unknown rule Kind '{0}' for host '{1}'; skipping" -f $rule.Kind, $HostKind)
            }
        }
    }

    # If any Strip rules fired, tidy up blank-line clusters that get left behind.
    if ($appliedStrip) {
        $result = [regex]::Replace($result, '(?m)(^\s*$\r?\n){3,}', "`r`n`r`n")
    }

    return $result
}

# Phase D cleanup 2026-05-24: removed back-compat helpers Get-SpecrewSquadRuntimePathDirectivePatterns
# and Get-SpecrewSlashCommandToPwshFormMap — both had zero callers across scripts/, hosts/, extensions/,
# Specrew.psm1, and tests (verified via deep-review agent). Introspection of per-host rules now goes
# directly through Get-SpecrewHostCoordinatorRules -HostKind <kind>.