diff --git a/scripts/powershell/create-new-feature.ps1 b/scripts/powershell/create-new-feature.ps1 index 2f23283fc4..2835c3ee4e 100644 --- a/scripts/powershell/create-new-feature.ps1 +++ b/scripts/powershell/create-new-feature.ps1 @@ -350,7 +350,10 @@ if (-not $DryRun) { if (-not (Test-Path -PathType Leaf $specFile)) { $template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot if ($template -and (Test-Path $template)) { - Copy-Item $template $specFile -Force + # Read the template content and write it to the spec file with UTF-8 encoding without BOM + $content = [System.IO.File]::ReadAllText($template) + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($specFile, $content, $utf8NoBom) } else { New-Item -ItemType File -Path $specFile -Force | Out-Null } diff --git a/scripts/powershell/setup-plan.ps1 b/scripts/powershell/setup-plan.ps1 index ee09094bf7..2881c9fc8a 100644 --- a/scripts/powershell/setup-plan.ps1 +++ b/scripts/powershell/setup-plan.ps1 @@ -34,8 +34,10 @@ New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null # Copy plan template if it exists, otherwise note it or create empty file $template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT if ($template -and (Test-Path $template)) { - Copy-Item $template $paths.IMPL_PLAN -Force - Write-Output "Copied plan template to $($paths.IMPL_PLAN)" + # Read the template content and write it to the implementation plan file with UTF-8 encoding without BOM + $content = [System.IO.File]::ReadAllText($template) + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom) } else { Write-Warning "Plan template not found" # Create a basic plan file if template doesn't exist diff --git a/tests/test_timestamp_branches.py b/tests/test_timestamp_branches.py index 39228d9455..078088b4c5 100644 --- a/tests/test_timestamp_branches.py +++ b/tests/test_timestamp_branches.py @@ -649,7 +649,25 @@ def test_powershell_surfaces_checkout_errors(self): contents = CREATE_FEATURE_PS.read_text(encoding="utf-8") assert "$switchBranchError = git checkout -q $branchName 2>&1 | Out-String" in contents assert "exists but could not be checked out.`n$($switchBranchError.Trim())" in contents + + @pytest.mark.skipif(not _has_pwsh(), reason="pwsh not installed") + def test_ps_spec_file_written_without_bom(self, ps_git_repo: Path): + """spec.md generated by create-new-feature.ps1 must not contain a UTF-8 BOM.""" + result = run_ps_script( + ps_git_repo, "-ShortName", "bom-check", "BOM check feature" + ) + assert result.returncode == 0, result.stderr + + # Find the generated spec.md + spec_file = next((ps_git_repo / "specs").rglob("spec.md"), None) + assert spec_file is not None, "spec.md was not created" + # Read the first 3 raw bytes and assert no BOM present + with open(spec_file, "rb") as f: + raw_bytes = f.read(3) + assert raw_bytes != b"\xef\xbb\xbf", ( + f"spec.md contains a UTF-8 BOM — found {raw_bytes!r} at start of file" + ) class TestGitExtensionParity: def test_bash_extension_surfaces_checkout_errors(self):