private/Write-WayforgeOpencodeAdapter.ps1

function Write-WayforgeOpencodeAdapter {
    <#
    .SYNOPSIS
        Generates an opencode plugin (.opencode/plugins/wayforge-gate.js) that
        blocks a tool call by delegating to the shared gate shim.
    .DESCRIPTION
        opencode runs the plugin under Bun with no build step. The plugin's
        `tool.execute.before` hook runs gate.ps1 and throws (opencode's documented
        block mechanism) when the gate denies (non-zero exit).
    #>

    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 '.opencode/plugins'
    if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    $path = Join-Path $dir 'wayforge-gate.js'

    $content = @'
// Generated by PSWayforge (Sync-WayforgeHarness -Harness opencode). Do not edit.
// Blocks a tool call by delegating to the shared Wayforge gate shim.
import { spawnSync } from "node:child_process";
 
export const WayforgeGate = async ({ directory }) => {
  const root = directory ? String(directory).replace(/\\/g, "/").replace(/\/$/, "") + "/" : "";
  const script = root + ".workflow/hooks/gate.ps1";
  return {
    "tool.execute.before": async (input, output) => {
      const payload = JSON.stringify({ tool_name: input.tool, tool_input: output.args });
      const r = spawnSync("pwsh", ["-NoProfile", "-File", script, "-Stage", "pre-tool", "-AsHook", "opencode"], { input: payload, encoding: "utf8" });
      if (r.status !== 0) {
        const reason = ((r.stdout || "") + (r.stderr || "")).trim();
        throw new Error(reason || `Wayforge gate blocked this tool (exit ${r.status}).`);
      }
    },
  };
};
'@


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