diff --git a/.conductor/registry/scripts/worktree-manager.ps1 b/.conductor/registry/scripts/worktree-manager.ps1 index 585e47f..689c263 100644 --- a/.conductor/registry/scripts/worktree-manager.ps1 +++ b/.conductor/registry/scripts/worktree-manager.ps1 @@ -251,7 +251,8 @@ try { exit 0 } - # teardown + # teardown — retry-with-backoff for transient file-handle races on Windows. + # Mirrors the delay schedule in GitWorktreeDeleter.cs:RemoveWithRetryAsync. if (-not (Test-Path $worktreePath)) { # Idempotent: nothing to clean up. $envelope.success = $true @@ -259,10 +260,21 @@ try { exit 0 } - $output = & git worktree remove --force $worktreePath 2>&1 - if ($LASTEXITCODE -ne 0) { + $wtDelays = @(0, 200, 500, 1000) + $wtOutput = $null + $wtSuccess = $false + foreach ($wtDelayMs in $wtDelays) { + if ($wtDelayMs -gt 0) { Start-Sleep -Milliseconds $wtDelayMs } + $wtOutput = & git worktree remove --force $worktreePath 2>&1 + if ($LASTEXITCODE -eq 0 -or -not (Test-Path $worktreePath)) { + $wtSuccess = $true + break + } + } + + if (-not $wtSuccess) { $envelope.error_code = 'worktree_remove_failed' - $envelope.error_message = "git worktree remove failed: $($output -join "`n")" + $envelope.error_message = "git worktree remove failed: $($wtOutput -join "`n")" $envelope | ConvertTo-Json -Compress exit 0 } diff --git a/scripts/Invoke-PolyphonySdlc.ps1 b/scripts/Invoke-PolyphonySdlc.ps1 index b8202ed..a072a23 100644 --- a/scripts/Invoke-PolyphonySdlc.ps1 +++ b/scripts/Invoke-PolyphonySdlc.ps1 @@ -217,6 +217,22 @@ if ($Intent -ne 'reset') { $script:LayoutDoc = 'docs/per-run-worktree-layout.md' +# ─── Helper: sanitize $Comment against shell-injection surfaces ────────────── +# +# $Comment flows into a here-string that is passed to pwsh -Command in the +# reset-detach path. Newlines (CR, LF, backtick-continuations) and bare +# backticks can break out of the embedded command string and inject arbitrary +# PowerShell. Strip them at the boundary before any interpolation. + +function Get-SanitizedComment { + param([Parameter(Mandatory)][AllowEmptyString()][string]$Value) + # Strip CR and LF characters. + $sanitized = $Value -replace '[\r\n]', '' + # Escape backticks (PowerShell escape character) so they are literal. + $sanitized = $sanitized -replace '`', '``' + return $sanitized +} + # ─── Helper: canonical path comparison (boundary-aware, OS-aware) ───────────── function Get-CanonicalPath { @@ -313,6 +329,9 @@ if ($Intent -eq 'reset') { } } + # Sanitize $Comment at the boundary before any shell interpolation. + $Comment = Get-SanitizedComment -Value $Comment + # Auto-detect platform from origin for gh-identity pinning. The reset # workflow itself does not take a platform input — the projection reset # executor resolves the platform internally (the PR-abandonment leg @@ -830,10 +849,15 @@ $detectedPlatform = $null $detectedRepository = $null $detectedRepoOrg = $null $detectedRepoProject = $null -$remoteUrl = & git -C $mainWorktree remote get-url origin 2>&1 -if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($remoteUrl)) { - $remoteUrl = $null - $global:LASTEXITCODE = 0 +Push-Location -LiteralPath $mainWorktree +try { + $remoteUrl = & git remote get-url origin 2>&1 + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($remoteUrl)) { + $remoteUrl = $null + $global:LASTEXITCODE = 0 + } +} finally { + Pop-Location } if ($remoteUrl) {