private/Write-WayforgeClaudeAdapter.ps1

function Write-WayforgeClaudeAdapter {
    <#
    .SYNOPSIS
        Projects a workflow's gates into .claude/settings.json.
    .DESCRIPTION
        Registers the shared gate shim at PreToolUse / Stop for gates whose
        `on` includes pre-tool / stop, and projects `forbid` gates into
        permissions.deny (fail-closed). Only the stages Claude can enforce are
        consumed here; pre-commit/pre-push/ci belong to the git + CI adapter.
    #>

    param([string] $ProjectRoot, [string] $WorkflowName)

    $set   = Get-WayforgeGateSet -Root $ProjectRoot -WorkflowName $WorkflowName
    $gates = $set.Gates

    $hasPreTool = $false
    $hasStop    = $false
    $denies     = [System.Collections.Generic.List[string]]::new()

    foreach ($gate in $gates) {
        $on = @(Get-WayforgeField $gate 'on')
        if ('pre-tool' -in $on) { $hasPreTool = $true }
        if ('stop' -in $on)     { $hasStop = $true }

        $forbid = Get-WayforgeField (Get-WayforgeField $gate 'check') 'forbid'
        if ($forbid) {
            $paths = @(Get-WayforgeField $forbid 'path')
            $tools = @(Get-WayforgeField $forbid 'tool')
            if (-not $tools) { $tools = @('edit', 'write') }
            foreach ($path in $paths) {
                foreach ($tool in $tools) {
                    $cap = switch ($tool) {
                        'edit'  { 'Edit' }  'write' { 'Write' }  'bash' { 'Bash' }
                        default { (Get-Culture).TextInfo.ToTitleCase($tool) }
                    }
                    $denies.Add("$cap($path)")
                }
            }
        }
    }

    # Literal ${CLAUDE_PROJECT_DIR} must survive into the JSON (single-quoted).
    $shim = '"${CLAUDE_PROJECT_DIR}/.workflow/hooks/gate.ps1"'
    $mkCommand = { param($stage) "pwsh -NoProfile -File $shim -Stage $stage -AsHook claude" }

    $hooks = [ordered]@{}
    if ($hasPreTool) {
        $hooks['PreToolUse'] = @(
            [ordered]@{
                matcher = 'Edit|Write|MultiEdit|NotebookEdit|Bash'
                hooks   = @([ordered]@{ type = 'command'; command = (& $mkCommand 'pre-tool') })
            }
        )
    }
    if ($hasStop) {
        $hooks['Stop'] = @(
            [ordered]@{ hooks = @([ordered]@{ type = 'command'; command = (& $mkCommand 'stop') }) }
        )
    }

    # Merge into any existing settings.json rather than replacing it, so unrelated
    # hooks, permissions, and other keys the user maintains are preserved. Our own
    # entries (identified by the gate.ps1 command) are refreshed idempotently.
    $claudeDir    = Join-Path $ProjectRoot '.claude'
    $settingsPath = Join-Path $claudeDir 'settings.json'

    $settings = @{}
    if (Test-Path $settingsPath -PathType Leaf) {
        try { $settings = Get-Content $settingsPath -Raw | ConvertFrom-Json -AsHashtable }
        catch { Write-Warning "Existing .claude/settings.json is not valid JSON; it will be replaced."; $settings = @{} }
    }
    if ($null -eq $settings) { $settings = @{} }

    if ($denies.Count -gt 0) {
        if ($settings['permissions'] -isnot [System.Collections.IDictionary]) { $settings['permissions'] = @{} }
        $merged = [System.Collections.Generic.List[string]]::new()
        foreach ($d in @($settings['permissions']['deny'])) { if ($d -and -not $merged.Contains($d)) { $merged.Add($d) | Out-Null } }
        foreach ($d in $denies) { if (-not $merged.Contains($d)) { $merged.Add($d) | Out-Null } }
        $settings['permissions']['deny'] = $merged.ToArray()
    }

    if ($hooks.Count -gt 0) {
        if ($settings['hooks'] -isnot [System.Collections.IDictionary]) { $settings['hooks'] = @{} }
        foreach ($event in $hooks.Keys) {
            # Keep the user's own entries for this event; drop any prior Wayforge
            # entry (whose command references gate.ps1) before adding the current one.
            $kept = foreach ($entry in (@($settings['hooks'][$event]) | Where-Object { $_ })) {
                $cmds = @($entry.hooks) | ForEach-Object { $_.command }
                if ($cmds -match 'gate\.ps1') { continue }
                $entry
            }
            $settings['hooks'][$event] = @(@($kept) + @($hooks[$event])) | Where-Object { $_ }
        }
    }

    if (-not (Test-Path $claudeDir)) { New-Item -ItemType Directory -Path $claudeDir -Force | Out-Null }
    ($settings | ConvertTo-Json -Depth 8) | Set-Content -Path $settingsPath -Encoding utf8NoBOM
    return $settingsPath
}