#!/usr/bin/env pwsh # orchestrator.ps1 — Pipeline V2: CODEUR → VERIFICATEUR → retry si echec # Usage: .\orchestrator.ps1 specs\ma-spec.md # Usage: .\orchestrator.ps1 specs\ma-spec.md -MaxRetries 3 # # Passe 1 (CODEUR): execute la spec # Passe 2 (VERIFICATEUR): verifie build, tests, grep checks # Si echec: re-lance le CODEUR avec le rapport d'erreur (max N retries) param( [Parameter(Mandatory=$true, Position=0)] [string]$SpecFile, [int]$MaxRetries = 2, [switch]$NoCommit, [switch]$NoPush, [switch]$NoNotify ) $ErrorActionPreference = "Continue" $REPO = "C:\Projects\metallkart-erp" $SCRIPTS = "$REPO\scripts" $LOGS = "$REPO\logs" $VPS = "root@76.13.55.81" # Resolve spec if (-not [System.IO.Path]::IsPathRooted($SpecFile)) { $SpecFile = Join-Path $REPO $SpecFile } if (-not (Test-Path $SpecFile)) { Write-Host "ERREUR: $SpecFile introuvable" -ForegroundColor Red exit 1 } $specName = [System.IO.Path]::GetFileNameWithoutExtension($SpecFile) $startTime = Get-Date Write-Host "╔════════════════════════════════════════╗" -ForegroundColor Magenta Write-Host "║ ORCHESTRATOR V2 — $specName" -ForegroundColor Magenta Write-Host "║ MaxRetries: $MaxRetries" -ForegroundColor DarkMagenta Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Magenta if (-not $NoNotify) { & "$SCRIPTS\notify-telegram.ps1" "ORCHESTRATOR [$specName] demarre (max $MaxRetries retries)" } function Invoke-Claude($prompt, $label) { $wrapperPath = "$env:TEMP\orch-$label-$specName.ps1" $outputPath = "$env:TEMP\orch-output-$label-$specName.txt" $exitCodePath = "$env:TEMP\orch-exit-$label-$specName.txt" $escapedPrompt = $prompt -replace "'","''" $wrapperLines = @( "Set-Location '$REPO'" "try {" " `$out = claude -p '$escapedPrompt' --allowedTools 'Bash(*),Read,Write,Edit,Glob,Grep' 2>&1 | Out-String" " `$out | Out-File -FilePath '$outputPath' -Encoding utf8" " `$LASTEXITCODE | Out-File -FilePath '$exitCodePath' -Encoding utf8" "} catch {" " `$_.Exception.Message | Out-File -FilePath '$outputPath' -Encoding utf8" " '1' | Out-File -FilePath '$exitCodePath' -Encoding utf8" "}" ) [System.IO.File]::WriteAllLines($wrapperPath, $wrapperLines, [System.Text.Encoding]::UTF8) $t0 = Get-Date Start-Process powershell.exe -ArgumentList "-NoProfile","-ExecutionPolicy","Bypass","-File",$wrapperPath -Wait -NoNewWindow $secs = [math]::Round(((Get-Date) - $t0).TotalSeconds, 1) $output = "" $exitCode = 0 if (Test-Path $outputPath) { $output = Get-Content $outputPath -Raw -ErrorAction SilentlyContinue } if (Test-Path $exitCodePath) { $codeStr = (Get-Content $exitCodePath -Raw -ErrorAction SilentlyContinue) if ($codeStr) { $codeStr = $codeStr.Trim(); if ($codeStr -match '^\d+$') { $exitCode = [int]$codeStr } } } Remove-Item $wrapperPath,$outputPath,$exitCodePath -Force -ErrorAction SilentlyContinue return @{ output=$output; exitCode=$exitCode; elapsed=$secs } } # ======================== # PASSE 1: CODEUR # ======================== $attempt = 0 $coderSuccess = $false while ($attempt -le $MaxRetries -and -not $coderSuccess) { $attempt++ Write-Host "`n┌─── CODEUR (tentative $attempt/$($MaxRetries+1)) ───" -ForegroundColor Yellow if ($attempt -eq 1) { $coderPrompt = "Lis le fichier $SpecFile et execute TOUTES les instructions. Supprime le fichier spec quand termine." } else { $coderPrompt = "RETRY $attempt — Le verificateur a trouve des erreurs. Lis $SpecFile (la spec originale) puis corrige les problemes suivants:`n$verifyErrors`nNe recommence PAS tout depuis zero, corrige uniquement les erreurs." } $coderResult = Invoke-Claude $coderPrompt "coder-$attempt" Write-Host " Codeur: exit $($coderResult.exitCode) en $($coderResult.elapsed)s" -ForegroundColor $(if($coderResult.exitCode -eq 0){"Green"}else{"Red"}) if ($coderResult.exitCode -ne 0) { Write-Host " Codeur CRASH — retry..." -ForegroundColor Red $verifyErrors = "Le codeur a crash avec exit code $($coderResult.exitCode). Output: $($coderResult.output.Substring(0, [Math]::Min(1000, $coderResult.output.Length)))" continue } # ======================== # PASSE 2: VERIFICATEUR # ======================== Write-Host "`n├─── VERIFICATEUR ───" -ForegroundColor Cyan $verifyPrompt = @" Tu es le VERIFICATEUR. Verifie que la spec '$specName' a ete correctement implementee: 1. BUILD: lance 'npm run build --prefix frontend' (si frontend touche) et verifie 0 erreurs 2. TYPESCRIPT: verifie que 'npx tsc --noEmit' passe (backend) 3. TESTS E2E: lance 'npx tsx tests/e2e-workflow.ts' et verifie tous les checks passent 4. GREP: verifie que les fichiers modifies existent et contiennent le code attendu 5. SERVEUR: verifie que le backend demarre sans erreur sur port 3001 (kill d'abord avec fuser -k 3001/tcp ou equivalent Windows) Retourne un JSON sur une seule ligne: {"pass":true/false,"checks":["check1: OK","check2: FAIL reason"],"errors":"description si fail"} IMPORTANT: retourne UNIQUEMENT le JSON, rien d'autre. "@ $verifyResult = Invoke-Claude $verifyPrompt "verify-$attempt" Write-Host " Verificateur: exit $($verifyResult.exitCode) en $($verifyResult.elapsed)s" -ForegroundColor $(if($verifyResult.exitCode -eq 0){"Green"}else{"Red"}) # Parse result $verifyPass = $false $verifyErrors = "" try { # Extract JSON from output (may have noise around it) $jsonMatch = [regex]::Match($verifyResult.output, '\{[^{}]*"pass"\s*:\s*(true|false)[^{}]*\}') if ($jsonMatch.Success) { $parsed = $jsonMatch.Value | ConvertFrom-Json $verifyPass = $parsed.pass if (-not $verifyPass) { $verifyErrors = $parsed.errors if ($parsed.checks) { $verifyErrors += "`nChecks: " + ($parsed.checks -join "`n") } } } else { $verifyErrors = "Verificateur n'a pas retourne de JSON valide. Output: $($verifyResult.output.Substring(0, [Math]::Min(500, $verifyResult.output.Length)))" } } catch { $verifyErrors = "Parse error verificateur: $($_.Exception.Message)" } if ($verifyPass) { $coderSuccess = $true Write-Host " ✓ VERIFICATION REUSSIE" -ForegroundColor Green } else { Write-Host " ✗ VERIFICATION ECHOUEE" -ForegroundColor Red Write-Host " Erreurs: $verifyErrors" -ForegroundColor DarkRed if ($attempt -le $MaxRetries) { Write-Host " → Retry codeur..." -ForegroundColor Yellow } } Write-Host "└───────────────────────" -ForegroundColor DarkGray } $totalElapsed = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 1) # Save log $logFile = Join-Path $LOGS "orch-$specName-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" $logContent = "=== ORCHESTRATOR $specName ===`nAttempts: $attempt`nSuccess: $coderSuccess`nElapsed: ${totalElapsed}s`n" [System.IO.File]::WriteAllText($logFile, $logContent, [System.Text.Encoding]::UTF8) if (-not $coderSuccess) { Write-Host "`n╔════════════════════════════════════════╗" -ForegroundColor Red Write-Host "║ ECHEC apres $attempt tentatives (${totalElapsed}s)" -ForegroundColor Red Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Red if (-not $NoNotify) { & "$SCRIPTS\notify-telegram.ps1" -Status "ERREUR" -TaskId "ORCH-$specName" -Elapsed $totalElapsed -Details "Echec apres $attempt tentatives" } exit 1 } # --- Git commit + push --- if (-not $NoCommit) { Write-Host "`nGit commit..." -ForegroundColor Yellow Set-Location $REPO $changes = git status --porcelain 2>&1 if ($changes) { git add -A git commit -m "spec: $specName (orchestrator v2, $attempt tentative(s))`n`nCo-Authored-By: Claude Opus 4.6 (1M context) " Write-Host " Committed" -ForegroundColor Green } } if (-not $NoPush -and -not $NoCommit) { Write-Host "Git push..." -ForegroundColor Yellow $unpushed = git log "origin/master..HEAD" --oneline 2>&1 if ($unpushed) { git push origin master 2>&1 Write-Host " Pushed" -ForegroundColor Green } scp "$REPO\MEMORY.md" "${VPS}:/opt/erp/metallkart-erp/MEMORY.md" 2>$null Write-Host " MEMORY.md synced" -ForegroundColor Magenta } # --- Final notification --- if (-not $NoNotify) { & "$SCRIPTS\notify-telegram.ps1" -Status "OK" -TaskId "ORCH-$specName" -Elapsed $totalElapsed -Details "$attempt tentative(s)" } Write-Host "`n╔════════════════════════════════════════╗" -ForegroundColor Green Write-Host "║ SUCCES: $specName (${totalElapsed}s, $attempt tentative(s))" -ForegroundColor Green Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Green