private/Write-WayforgePiAdapter.ps1

function Write-WayforgePiAdapter {
    <#
    .SYNOPSIS
        Generates a pi extension (.pi/extensions/wayforge-gate.ts) that blocks a
        tool call by delegating to the shared gate shim.
    .DESCRIPTION
        pi loads the extension via jiti (no build). The `tool_call` handler runs
        gate.ps1 and returns { block: true, reason } when the gate denies
        (non-zero exit). Project extensions load only after the project is trusted.
    #>

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

    $set    = Get-WayforgeGateSet -Root $ProjectRoot -WorkflowName $WorkflowName
    $stages = Get-WayforgeStages -Gates $set.Gates
    if (-not $stages['pre-tool']) { return $null }

    $dir = Join-Path $ProjectRoot '.pi/extensions'
    if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    $path = Join-Path $dir 'wayforge-gate.ts'

    $content = @'
// Generated by PSWayforge (Sync-WayforgeHarness -Harness pi). Do not edit.
// Blocks a tool call by delegating to the shared Wayforge gate shim.
import { spawnSync } from "node:child_process";
import { join } from "node:path";
 
export default function (pi: any) {
  pi.on("tool_call", async (event: any, ctx: any) => {
    const cwd = ctx && ctx.cwd ? ctx.cwd : process.cwd();
    const script = join(cwd, ".workflow", "hooks", "gate.ps1");
    const payload = JSON.stringify({ tool_name: event.toolName, tool_input: event.input });
    const r = spawnSync("pwsh", ["-NoProfile", "-File", script, "-Stage", "pre-tool", "-AsHook", "pi"], { input: payload, encoding: "utf8" });
    if (r.status !== 0) {
      const reason = ((r.stdout || "") + (r.stderr || "")).trim();
      return { block: true, reason: reason || `Wayforge gate blocked this tool (exit ${r.status}).` };
    }
  });
}
'@


    Set-Content -Path $path -Value $content -Encoding utf8NoBOM
    return $path
}