Payload/scripts/merge-harness-to-kaden.ps1

#Requires -Version 5.1
<#
.SYNOPSIS
  Surgically merge matured harness surfaces from this child into a local Kaden clone.

.DESCRIPTION
  Layout contract (post scripts/harness/ restructure):

  | Source (this repo) | Kaden destination |
  |---------------------------------|-----------------------------------------------------------|
  | scripts/harness/** | templates/scripts/harness/** AND scripts/harness/** |
  | pwsh -File scripts/harness-invoke.ps1 | templates/scripts/ + scripts/ (stable entry point) |
  | scripts/verify-local.ps1 | templates/scripts/ + scripts/ (stable entry point) |
  | scripts/lift|merge-*.ps1 | templates/scripts/ + scripts/ (promote tooling) |
  | docs/kaden/** | docs/kaden/** |
  | tests/harness/** | tests/harness/** |
  | selected .github skills/evals | .github/... |

  Removes obsolete flat copies under templates/scripts/ and scripts/ that
  moved into scripts/harness/ (harness-*.ps1, lib/, lint-*.mjs, etc.).

  Prefer UTF-8 Evidence-First inject on template agents over wholesale agent copy.
  Does NOT commit. Does NOT delete attribution/README files.

.EXAMPLE
  .\scripts\merge-harness-to-kaden.ps1 -TemplateRoot $env:USERPROFILE\Repos_local\kaden
#>

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

    [string]$SourceRoot = ''
)

$ErrorActionPreference = 'Stop'
$utf8NoBom = New-Object System.Text.UTF8Encoding $false

if ([string]::IsNullOrWhiteSpace($SourceRoot)) {
    $SourceRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
}
$TemplateRoot = (Resolve-Path $TemplateRoot).Path

$EfBullet = '- Follow [Evidence-First Work Ethic](../skills/evidence-first-work-ethic/SKILL.md) - truth over pleasing; inspect before asserting; verify before claiming done; block instead of guessing.'

function New-HarnessLiftDirectory {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([Parameter(Mandatory = $true)][string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) {
        if ($PSCmdlet.ShouldProcess($Path, 'Create directory')) {
            New-Item -ItemType Directory -Path $Path -Force | Out-Null
        }
    }
}

function Write-Utf8File([string]$Path, [string]$Content) {
    [IO.File]::WriteAllText($Path, $Content, $utf8NoBom)
}

function Copy-HarnessLiftFile {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([string]$Rel, [string]$DestRel = '')
    if ([string]::IsNullOrWhiteSpace($DestRel)) { $DestRel = $Rel }
    $src = Join-Path $SourceRoot $Rel
    $dst = Join-Path $TemplateRoot $DestRel
    if (-not (Test-Path -LiteralPath $src)) {
        Write-Warning "Skip missing: $Rel"
        return
    }
    if (-not $PSCmdlet.ShouldProcess($dst, "Copy $Rel")) { return }
    New-HarnessLiftDirectory (Split-Path $dst -Parent)
    Copy-Item -LiteralPath $src -Destination $dst -Force
    Write-Output "OK file $Rel -> $DestRel"
}

function Copy-HarnessLiftDirectory {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([string]$Rel, [string]$DestRel = '')
    if ([string]::IsNullOrWhiteSpace($DestRel)) { $DestRel = $Rel }
    $src = Join-Path $SourceRoot $Rel
    $dst = Join-Path $TemplateRoot $DestRel
    if (-not (Test-Path -LiteralPath $src)) {
        Write-Warning "Skip missing dir: $Rel"
        return
    }
    if (-not $PSCmdlet.ShouldProcess($dst, "Copy dir $Rel")) { return }
    New-HarnessLiftDirectory (Split-Path $dst -Parent)
    if (Test-Path -LiteralPath $dst) { Remove-Item -LiteralPath $dst -Recurse -Force }
    Copy-Item -LiteralPath $src -Destination $dst -Recurse -Force
    Write-Output "OK dir $Rel -> $DestRel"
}

function Remove-ObsoleteFlatHarnessFile {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([Parameter(Mandatory = $true)][string]$Rel)
    $dst = Join-Path $TemplateRoot $Rel
    if (-not (Test-Path -LiteralPath $dst)) { return }
    if ($PSCmdlet.ShouldProcess($dst, 'Remove obsolete flat harness copy')) {
        Remove-Item -LiteralPath $dst -Force -Recurse
        Write-Output "RM obsolete $Rel"
    }
}

function Add-HarnessEvidenceFirstConstraint {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param([Parameter(Mandatory = $true)][string]$AgentPath)
    $text = [IO.File]::ReadAllText($AgentPath)
    if ($text -match 'Evidence-First Work Ethic') {
        Write-Output "skip EF (present): $(Split-Path $AgentPath -Leaf)"
        return
    }
    $patterns = @(
        '(?m)^(## Constraints(?: and Boundaries)?\r?\n\r?\n)',
        '(?m)^(## Constraints(?: and Boundaries)?\r?\n)',
        '(?m)^(## Operating Rules\r?\n\r?\n)',
        '(?m)^(## Operating Rules\r?\n)',
        '(?m)^(## Do / Don''t\r?\n\r?\n)',
        '(?m)^(## Do / Don''t\r?\n)'
    )
    $updated = $false
    foreach ($pat in $patterns) {
        if ($text -match $pat) {
            $text = [regex]::Replace($text, $pat, ('$1' + $EfBullet + "`r`n"), 1)
            $updated = $true
            break
        }
    }
    if (-not $updated) {
        Write-Warning "No Constraints heading: $(Split-Path $AgentPath -Leaf)"
        return
    }
    if (-not $PSCmdlet.ShouldProcess($AgentPath, 'Inject Evidence-First Constraints')) { return }
    Write-Utf8File $AgentPath $text
    Write-Output "OK EF inject: $(Split-Path $AgentPath -Leaf)"
}

Write-Output "Source: $SourceRoot"
Write-Output "Kaden: $TemplateRoot"

# --- Skills (mac ahead) ---
foreach ($skill in @(
        'evidence-first-work-ethic',
        'repo-agent-basics',
        'artifact-paths',
        'review-loop-contract',
        'harness-operator-guide',
        'agent-evals',
        'agent-skill-authoring',
        'sub-agent-communication-protocol',
        'agentic-spending-governance',
        'model-routing',
        'code-review'
    )) {
    Copy-HarnessLiftDirectory ".github\skills\$skill"
}

Copy-HarnessLiftFile '.github\agents\feature-orchestrator.agent.md'

# --- Evals (fixtures only; skip captures / last-eval runtime) ---
New-HarnessLiftDirectory (Join-Path $TemplateRoot '.github\evals\skills')
foreach ($ev in @('evidence-first-work-ethic', 'repo-agent-basics', 'artifact-paths')) {
    Copy-HarnessLiftDirectory ".github\evals\skills\$ev"
}
Copy-HarnessLiftFile '.github\evals\README.md'

# --- Instructions / copilot / prompts ---
Copy-HarnessLiftFile '.github\instructions\evidence-first.instructions.md'
Copy-HarnessLiftFile '.github\copilot-instructions.md'
foreach ($p in @('feature-start.prompt.md', 'feature-resume.prompt.md', 'agent-eval.prompt.md')) {
    Copy-HarnessLiftFile ".github\prompts\$p"
}

# --- Templates (hooks + review-loop) ---
New-HarnessLiftDirectory (Join-Path $TemplateRoot '.github\templates')
New-HarnessLiftDirectory (Join-Path $TemplateRoot 'templates')
Copy-HarnessLiftFile '.github\templates\cursor-hooks.json'
Copy-HarnessLiftFile '.github\templates\cursor-hooks.json' 'templates\cursor-hooks.json'
Copy-HarnessLiftFile '.github\templates\copilot-hooks.json'
Copy-HarnessLiftFile '.github\templates\copilot-hooks.json' 'templates\copilot-hooks.json'
Copy-HarnessLiftFile '.github\templates\review-loop.json' 'templates\review-loop.json'
Copy-HarnessLiftFile '.github\templates\harness-requirements.json' 'templates\harness-requirements.json'
Copy-HarnessLiftFile '.github\templates\review-loop.json' '.github\templates\review-loop.json'
Copy-HarnessLiftFile '.github\templates\harness-requirements.json' '.github\templates\harness-requirements.json'
Copy-HarnessLiftFile '.github\templates\handoff-slice.md' 'templates\handoff-slice.md'
Copy-HarnessLiftFile '.github\templates\handoff-slice.md'

# --- Kaden operator docs (child-safe folder; does not overwrite child docs root) ---
Copy-HarnessLiftDirectory 'docs\kaden' 'docs\kaden'
# ADR 0003 lives in the Kaden clone only; copy if the child still has it.
Copy-HarnessLiftFile 'docs\adr\0003-harness-event-journal-and-gate-projection.md'

# --- Rules (flat in template; pull maps to 00-from-template) ---
$rulesDir = Join-Path $TemplateRoot '.cursor\rules'
New-HarnessLiftDirectory $rulesDir
$ruleMap = @{
    'evidence-first-work-ethic.mdc'   = 'evidence-first-work-ethic.mdc'
    'core-philosophy.mdc'             = 'core-philosophy.mdc'
    'agent-ecosystem.mdc'             = 'agent-ecosystem.mdc'
    'agentic-spending-governance.mdc' = 'agentic-spending-governance.mdc'
    'local-verify-gate.mdc'           = 'local-verify-gate.mdc'
    'development-workflow.mdc'        = 'development-workflow.mdc'
}
foreach ($kv in $ruleMap.GetEnumerator()) {
    Copy-HarnessLiftFile ".cursor\rules\00-from-template\$($kv.Key)" ".cursor\rules\$($kv.Value)"
}

# --- Harness core tree (read-only in children) ---
Copy-HarnessLiftDirectory 'scripts\harness' 'templates\scripts\harness'
Copy-HarnessLiftDirectory 'scripts\harness' 'scripts\harness'

# --- Stable entry points + promote tooling (scripts/ root) ---
foreach ($f in @(
        'harness-invoke.ps1',
        'verify-local.ps1',
        'lift-harness-to-kaden.ps1',
        'merge-harness-to-kaden.ps1'
    )) {
    Copy-HarnessLiftFile "scripts\$f" "templates\scripts\$f"
    Copy-HarnessLiftFile "scripts\$f" "scripts\$f"
}

# --- Obsolete flat copies (pre-restructure layout) ---
$obsoleteFlat = @(
    'harness-dispatch-preflight.ps1',
    'harness-health.ps1',
    'harness-journal.ps1',
    'harness-own-slice-status.ps1',
    'harness-own-slice.ps1',
    'harness-persist-quality.ps1',
    'harness-pretooluse-guard.ps1',
    'harness-record-handoff.ps1',
    'harness-record-phase.ps1',
    'harness-record-review.ps1',
    'harness-record-slice-verdict.ps1',
    'harness-record-task-step.ps1',
    'harness-record-task.ps1',
    'harness-relay.ps1',
    'harness-run-slice-tests.ps1',
    'harness-sensor.ps1',
    'harness-session-start.ps1',
    'harness-step.ps1',
    'harness-stop-gate.ps1',
    'harness-timeline.ps1',
    'harness-turn-start.ps1',
    'harness-turn-stop.ps1',
    'init-harness-feature.ps1',
    'validate-review-loop.ps1',
    'setup-harness-hooks.ps1',
    'pull-ai-base.ps1',
    'setup-ai-overlays.ps1',
    'sync-cursor-ai.ps1',
    'run-agent-eval.ps1',
    'run-agent-eval.mjs',
    'lint-powershell.ps1',
    'lint-tests.mjs',
    'lint-review-loop.mjs',
    'format-review-loop.mjs',
    'run-lint-staged.mjs',
    'run-npm-script.mjs',
    'resolve-node-for-git.sh',
    'stryker-run.mjs',
    'stryker-diff.mjs',
    'stryker-scope.mjs',
    'parse-mutation-report.mjs',
    'lib'
)
foreach ($leaf in $obsoleteFlat) {
    Remove-ObsoleteFlatHarnessFile "templates\scripts\$leaf"
    Remove-ObsoleteFlatHarnessFile "scripts\$leaf"
}

# --- Child PowerShellLintPaths: exclude harness/ as vendor ---
$childLintPaths = @'
# Executable scope for scripts/harness/lint-powershell.ps1 - copy to repo root and adapt.
#
# Only paths under LintRoots are scanned. This avoids node_modules, vendored
# trees, and repo-wide -Recurse over ".".
#
# Child repos: harness core lives under scripts/harness/ (and tests/harness/).
# ExcludeDirectoryNames includes "harness" so PSScriptAnalyzer skips read-only
# harness scripts. The harness source repo (mac-test-suite) intentionally does
# NOT exclude harness - it lints the core.

@{
    LintRoots = @(
        'scripts'
        'tests/manual'
        'hooks'
        '.githooks'
        'certs'
    )

    ExcludeDirectoryNames = @(
        'node_modules'
        '.venv'
        'wip'
        'ideas'
        'output'
        '__pycache__'
        '.pytest_cache'
        'harness'
    )
}
'@

$childLintRel = 'templates\PowerShellLintPaths.psd1'
$childLintDst = Join-Path $TemplateRoot $childLintRel
if ($PSCmdlet.ShouldProcess($childLintDst, 'Write child PowerShellLintPaths with harness exclusion')) {
    New-HarnessLiftDirectory (Split-Path $childLintDst -Parent)
    Write-Utf8File $childLintDst $childLintPaths
    Write-Output "OK file $childLintRel (child harness exclusion)"
}

# --- Pester ---
Copy-HarnessLiftDirectory 'tests\harness'

# --- Agents: UTF-8-safe EF inject (never robocopy child agents) ---
$agentDir = Join-Path $TemplateRoot '.github\agents'
if (Test-Path -LiteralPath $agentDir) {
    Get-ChildItem -LiteralPath $agentDir -Filter '*.agent.md' | ForEach-Object {
        Add-HarnessEvidenceFirstConstraint $_.FullName
    }
}

# Feature-orchestrator: ownership autonomy + journal (surgical, keep UTF-8 body)
$orch = Join-Path $agentDir 'feature-orchestrator.agent.md'
if (Test-Path -LiteralPath $orch) {
    $t = [IO.File]::ReadAllText($orch)
    if ($t -notmatch 'do not intercept that chain') {
        $oldLine = '- After every subagent: confirm artifacts on disk, append `.review-loop.json`, update `docs/harness/features/<slug>/handoff-<slice>.md`, emit a status table to the human.'
        $newBlock = @(
            '- After every subagent: confirm artifacts on disk, append journal start/stop and `.review-loop.json` via harness scripts, update `docs/harness/features/<slug>/handoff-<slice>.md`, emit a status table to the human.',
            '- **Visible progress is not a human approval gate.** The operator observes; they approve COMMIT and PR - not RED->GREEN or reviewer hops.',
            '- **`@atdd-developer` slice ownership** chains RED -> pre-commit reviewers until `review_ready` without per-hop human approval - do not intercept that chain.',
            '- **Never** black-box a full slice inside a nested Task without returning status - but do not ask the human to "approve GREEN" between steps.'
        ) -join "`r`n"
        if ($t.Contains($oldLine)) {
            $t = $t.Replace($oldLine, $newBlock)
        }
        elseif ($t -match '(?m)^- After every subagent:.*\r?\n') {
            $t = [regex]::Replace($t, '(?m)^- After every subagent:.*\r?\n', ($newBlock + "`r`n"), 1)
        }
        else {
            Write-Warning 'feature-orchestrator: could not find After every subagent line; ownership bullets not merged'
        }
    }
    if ($PSCmdlet.ShouldProcess($orch, 'Write feature-orchestrator ownership merge')) {
        Write-Utf8File $orch $t
        Write-Output 'OK feature-orchestrator ownership/journal merge'
    }
}

# atdd-developer: journal obligation in Constraints (UTF-8 template body)
$atdd = Join-Path $agentDir 'atdd-developer.agent.md'
if (Test-Path -LiteralPath $atdd) {
    $t = [IO.File]::ReadAllText($atdd)
    if ($t -notmatch 'harness-journal\.ps1|harness-turn-start') {
        $journalLine = '- Journal via harness DNA: `pwsh -File scripts/harness-invoke.ps1 harness-turn-start.ps1` / `harness-turn-stop.ps1` (and recorders); see [harness-journal.md](../../docs/kaden/harness-journal.md).'
        if ($t -match '(?m)^(- Follow \[Evidence-First Work Ethic\].*)\r?\n') {
            $t = [regex]::Replace($t, '(?m)^(- Follow \[Evidence-First Work Ethic\].*\r?\n)', ('$1' + $journalLine + "`r`n"), 1)
            Write-Utf8File $atdd $t
            Write-Output 'OK atdd-developer journal obligation'
        }
        else {
            Write-Warning 'atdd-developer: Evidence-First line not found for journal insert'
        }
    }
}

# Sync Cursor overlays in template
$sync = Join-Path $TemplateRoot 'scripts\harness\sync-cursor-ai.ps1'
if (-not (Test-Path -LiteralPath $sync)) {
    $sync = Join-Path $TemplateRoot 'scripts\sync-cursor-ai.ps1'
}
if (Test-Path -LiteralPath $sync) {
    if ($PSCmdlet.ShouldProcess($sync, 'Run sync-cursor-ai.ps1')) {
        & $sync -RepoRoot $TemplateRoot
    }
}

Write-Output ''
Write-Output 'Merge complete (no commit). Preserved: .cursor/README.md, AGENTS-ATTRIBUTION.md (not overwritten).'
Write-Output ('Next: review git status in {0}' -f $TemplateRoot)
Write-Output 'Then patch README / getting-started paths if needed, and in children:'
Write-Output ' .\scripts\harness\pull-ai-base.ps1 -TemplateRoot <path-to-kaden>'
Write-Output ' (first-time: also copy templates/pwsh -File scripts/harness-invoke.ps1 + verify-local.ps1 to scripts/)'