#requires -Version 5.1 <# .SYNOPSIS CASAN Plan-20 agentic-client installer for Windows PowerShell. .DESCRIPTION Installs, verifies (doctor) or removes the CASAN agentic-client integration (Claude Code and/or Codex project hooks) in a target repository — from a clean clone, with NO manual file copying. Every created file is recorded in a manifest so uninstall never touches a user's own config. The hooks call the CASAN agentic bridge, which never runs a model (Claude/ Codex remains the sole model executor). See: docs/casan/CASAN_AGENTIC_CLIENTS_WINDOWS.md docs/casan/CASAN_AGENTIC_CLIENT_SECURITY.md .PARAMETER Action install | doctor | uninstall (default: install) .PARAMETER Client claude | codex | all (default: all) .PARAMETER Target Target repository root. Default: the repo this script lives in. .PARAMETER Mode observe | enforce (default: observe) — sets the bridge enforcement mode written into the target's .casan/agentic.env. .EXAMPLE pwsh packages/casan-devkit/windows/install-agentic.ps1 -Client claude -Mode enforce .EXAMPLE pwsh packages/casan-devkit/windows/install-agentic.ps1 -Action doctor -Client all #> [CmdletBinding()] param( [ValidateSet('install', 'doctor', 'uninstall')] [string]$Action = 'install', [ValidateSet('claude', 'codex', 'all')] [string]$Client = 'all', [string]$Target, [ValidateSet('observe', 'enforce')] [string]$Mode = 'observe' ) $ErrorActionPreference = 'Stop' # ── Resolve roots ──────────────────────────────────────────────────────────── # This script lives at /packages/casan-devkit/windows/install-agentic.ps1 $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $RepoRoot = (Resolve-Path (Join-Path $ScriptDir '..\..\..')).Path if (-not $Target) { $Target = $RepoRoot } $Target = (Resolve-Path $Target).Path $HarnessRel = 'packages\casan-harness' $Bridge = Join-Path $Target "$HarnessRel\scripts\python\agentic_bridge.py" $ClaudeTpl = Join-Path $Target "$HarnessRel\adapters\claude-code\settings.template.json" $CodexHooks = Join-Path $Target "$HarnessRel\adapters\codex\hooks.template.json" $CodexConf = Join-Path $Target "$HarnessRel\adapters\codex\config.template.toml" $ManifestDir = Join-Path $Target '.casan' $Manifest = Join-Path $ManifestDir 'agentic-install-manifest.json' function Resolve-Python { foreach ($cand in @('python3', 'python', 'py')) { $cmd = Get-Command $cand -ErrorAction SilentlyContinue if ($cmd) { try { & $cmd.Source --version *> $null; if ($LASTEXITCODE -eq 0) { return $cmd.Source } } catch {} } } throw 'No working Python 3 interpreter found on PATH (need python3/python/py).' } function Write-Info($m) { Write-Host "[casan] $m" } function Write-Ok($m) { Write-Host "[casan] OK $m" -ForegroundColor Green } function Write-Warn2($m) { Write-Host "[casan] !! $m" -ForegroundColor Yellow } function Write-Err($m) { Write-Host "[casan] ERR $m" -ForegroundColor Red } function Load-Manifest { if (Test-Path $Manifest) { try { return Get-Content -Raw -Path $Manifest | ConvertFrom-Json } catch {} } return [PSCustomObject]@{ created = @(); clients = @() } } function Save-Manifest($m) { if (-not (Test-Path $ManifestDir)) { New-Item -ItemType Directory -Force -Path $ManifestDir | Out-Null } $m | ConvertTo-Json -Depth 6 | Set-Content -Path $Manifest -Encoding UTF8 } # Copy a template into place. Never overwrite a user's existing config silently: # back it up first and record both in the manifest so uninstall can restore it. function Install-File($src, $dst, $manifest) { $dstDir = Split-Path -Parent $dst if (-not (Test-Path $dstDir)) { New-Item -ItemType Directory -Force -Path $dstDir | Out-Null } if (Test-Path $dst) { $backup = "$dst.casan-bak" if (-not (Test-Path $backup)) { Copy-Item -Path $dst -Destination $backup -Force Write-Warn2 "existing $([System.IO.Path]::GetFileName($dst)) backed up to $([System.IO.Path]::GetFileName($backup))" } } Copy-Item -Path $src -Destination $dst -Force $entry = @{ path = $dst; from = $src } $manifest.created = @($manifest.created + $dst | Select-Object -Unique) Write-Ok "installed $dst" } function Do-Install { if (-not (Test-Path $Bridge)) { throw "Bridge not found at $Bridge — is this a CASAN repo?" } $py = Resolve-Python $manifest = Load-Manifest $clients = @() if ($Client -in @('claude', 'all')) { Install-File $ClaudeTpl (Join-Path $Target '.claude\settings.json') $manifest $clients += 'claude' } if ($Client -in @('codex', 'all')) { Install-File $CodexHooks (Join-Path $Target '.codex\hooks.json') $manifest Install-File $CodexConf (Join-Path $Target '.codex\config.toml') $manifest $clients += 'codex' Write-Warn2 'Codex loads project hooks only AFTER you accept its trust prompt — open the repo in Codex once to complete onboarding.' } # Bridge feature flags for the target (sourced by the user's shell/session). $envFile = Join-Path $Target '.casan\agentic.env' @( "# CASAN Plan-20 agentic bridge flags (Windows). Source before starting the client.", "CASAN_AGENTIC_BRIDGE_ENABLED=1", "CASAN_AGENTIC_ENFORCEMENT_MODE=$Mode", "CASAN_AGENTIC_INTEGRATION_MODE=project_hook" ) | Set-Content -Path $envFile -Encoding UTF8 $manifest.created = @($manifest.created + $envFile | Select-Object -Unique) Write-Ok "wrote flags -> $envFile (mode=$Mode)" $manifest.clients = @($clients | Select-Object -Unique) Save-Manifest $manifest Write-Info 'Running doctor to verify...' Do-Doctor Write-Info "Install complete. Set CASAN_AGENTIC_ENFORCEMENT_MODE=enforce when ready to certify turns." } function Do-Doctor { $py = Resolve-Python Write-Info "python : $py" Write-Info "target : $Target" Write-Info "bridge : $Bridge" if (-not (Test-Path $Bridge)) { Write-Err 'bridge missing'; exit 1 } # Core bridge self-diagnostics. & $py $Bridge doctor if ($LASTEXITCODE -ne 0) { Write-Err 'bridge doctor reported a problem'; } $ok = $true if ($Client -in @('claude', 'all')) { if (Test-Path (Join-Path $Target '.claude\settings.json')) { Write-Ok '.claude\settings.json present' } else { Write-Warn2 '.claude\settings.json missing (run install)'; $ok = $false } } if ($Client -in @('codex', 'all')) { if (Test-Path (Join-Path $Target '.codex\hooks.json')) { Write-Ok '.codex\hooks.json present (Codex trust review still required)' } else { Write-Warn2 '.codex\hooks.json missing (run install)'; $ok = $false } } # A real begin->finalize smoke turn against an isolated state dir. $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("casan-agentic-" + [guid]::NewGuid().ToString('N')) $env:CASAN_STATE_ROOT = (Join-Path $tmp '.specify') try { $begin = '{"op":"begin","client":"claude-code","project":"' + ($Target -replace '\\','/') + '","session":"doctor","prompt":"doctor smoke","integration_mode":"project_hook"}' $resp = $begin | & $py $Bridge run | ConvertFrom-Json if ($resp.decision -eq 'allow') { Write-Ok "smoke begin admitted (strength=$($resp.certification_strength))" } else { Write-Warn2 "smoke begin decision=$($resp.decision)"; } } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue Remove-Item Env:\CASAN_STATE_ROOT -ErrorAction SilentlyContinue } if ($ok) { Write-Ok 'doctor passed' } else { Write-Warn2 'doctor found missing config' } } function Do-Uninstall { $manifest = Load-Manifest if (-not $manifest.created -or $manifest.created.Count -eq 0) { Write-Warn2 'no install manifest found — nothing to remove' return } foreach ($f in $manifest.created) { if (Test-Path $f) { Remove-Item -Force $f Write-Ok "removed $f" } $backup = "$f.casan-bak" if (Test-Path $backup) { Move-Item -Force $backup $f Write-Ok "restored user's original $f from backup" } } Remove-Item -Force $Manifest -ErrorAction SilentlyContinue Write-Info 'Uninstall complete — your own (non-CASAN) config was preserved.' } switch ($Action) { 'install' { Do-Install } 'doctor' { Do-Doctor } 'uninstall' { Do-Uninstall } }