210 lines
9.3 KiB
PowerShell
210 lines
9.3 KiB
PowerShell
#!/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) <noreply@anthropic.com>"
|
|
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
|