scripts/specrew-init.ps1
|
[CmdletBinding(PositionalBinding = $false)] param( [Alias('project-path')] [string]$ProjectPath = (Get-Location).Path, [Alias('dry-run')] [switch]$DryRun, [switch]$Force, [Alias('speckit-version')] [string]$SpecKitVersion = '0.8.4', [Alias('squad-version')] [string]$SquadVersion = '0.9.1', [string]$Agents = 'copilot', [Alias('no-agents')] [switch]$NoAgents, [Alias('spec-kit-extension-only')] [switch]$SpecKitExtensionOnly, [switch]$SkipUpdateCheck, [switch]$Help, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$CliArgs ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $sharedGovernancePath = Join-Path (Split-Path -Parent $PSScriptRoot) 'extensions\specrew-speckit\scripts\shared-governance.ps1' if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) { throw "Missing shared governance helper '$sharedGovernancePath'." } . $sharedGovernancePath $versionCheckHelperPath = Join-Path $PSScriptRoot 'internal\version-check.ps1' if (-not (Test-Path -LiteralPath $versionCheckHelperPath -PathType Leaf)) { throw "Missing version-check helper '$versionCheckHelperPath'." } . $versionCheckHelperPath $skillCatalogStateHelperPath = Join-Path $PSScriptRoot 'internal\skill-catalog-state.ps1' if (-not (Test-Path -LiteralPath $skillCatalogStateHelperPath -PathType Leaf)) { throw "Missing skill-catalog state helper '$skillCatalogStateHelperPath'." } . $skillCatalogStateHelperPath $initUtilitiesPath = Join-Path $PSScriptRoot 'init\_utilities.ps1' if (-not (Test-Path -LiteralPath $initUtilitiesPath -PathType Leaf)) { throw "Missing init/_utilities.ps1 helper at '$initUtilitiesPath'." } . $initUtilitiesPath $initPreflightPath = Join-Path $PSScriptRoot 'init\preflight.ps1' if (-not (Test-Path -LiteralPath $initPreflightPath -PathType Leaf)) { throw "Missing init/preflight.ps1 helper at '$initPreflightPath'." } . $initPreflightPath $initTemplateDeployPath = Join-Path $PSScriptRoot 'init\template-deploy.ps1' if (-not (Test-Path -LiteralPath $initTemplateDeployPath -PathType Leaf)) { throw "Missing init/template-deploy.ps1 helper at '$initTemplateDeployPath'." } . $initTemplateDeployPath $initSpecKitDeployPath = Join-Path $PSScriptRoot 'init\spec-kit-deploy.ps1' if (-not (Test-Path -LiteralPath $initSpecKitDeployPath -PathType Leaf)) { throw "Missing init/spec-kit-deploy.ps1 helper at '$initSpecKitDeployPath'." } . $initSpecKitDeployPath $initDependencyInstallPath = Join-Path $PSScriptRoot 'init\dependency-install.ps1' if (-not (Test-Path -LiteralPath $initDependencyInstallPath -PathType Leaf)) { throw "Missing init/dependency-install.ps1 helper at '$initDependencyInstallPath'." } . $initDependencyInstallPath $initAgentDetectionPath = Join-Path $PSScriptRoot 'init\agent-detection.ps1' if (-not (Test-Path -LiteralPath $initAgentDetectionPath -PathType Leaf)) { throw "Missing init/agent-detection.ps1 helper at '$initAgentDetectionPath'." } . $initAgentDetectionPath $initSquadDeployPath = Join-Path $PSScriptRoot 'init\squad-deploy.ps1' if (-not (Test-Path -LiteralPath $initSquadDeployPath -PathType Leaf)) { throw "Missing init/squad-deploy.ps1 helper at '$initSquadDeployPath'." } . $initSquadDeployPath $initPostBootstrapPath = Join-Path $PSScriptRoot 'init\post-bootstrap-output.ps1' if (-not (Test-Path -LiteralPath $initPostBootstrapPath -PathType Leaf)) { throw "Missing init/post-bootstrap-output.ps1 helper at '$initPostBootstrapPath'." } . $initPostBootstrapPath $initCrewBootstrapPath = Join-Path $PSScriptRoot 'init\crew-bootstrap.ps1' if (-not (Test-Path -LiteralPath $initCrewBootstrapPath -PathType Leaf)) { throw "Missing init/crew-bootstrap.ps1 helper at '$initCrewBootstrapPath'." } . $initCrewBootstrapPath function Get-ManagedAgentsBlock { param( [Parameter(Mandatory = $true)] [pscustomobject[]]$Agents ) $lookup = Get-AgentLookup -Agents $Agents $lines = @( '# >>> specrew-managed agents >>>', '# Specrew-managed delegated-agent opt-in and detection state (FR-022).', 'agents:' ) foreach ($name in @('copilot', 'claude', 'codex')) { $agent = $lookup[$name] $lines += " ${name}:" $lines += " enabled: $(ConvertTo-YamlBoolean -Value $agent.Enabled)" $lines += " access_path: $($agent.AccessPath)" $lines += " availability: $($agent.Availability)" } $lines += '# <<< specrew-managed agents <<<' return $lines -join [Environment]::NewLine } function Set-IterationConfigAgents { param( [Parameter(Mandatory = $true)] [string]$IterationConfigPath, [Parameter(Mandatory = $true)] [pscustomobject[]]$Agents, [Parameter(Mandatory = $true)] [System.Collections.ArrayList]$Actions, [Parameter(Mandatory = $true)] [switch]$PreviewOnly ) $managedBlock = Get-ManagedAgentsBlock -Agents $Agents if (-not (Test-Path -LiteralPath $IterationConfigPath)) { if ($PreviewOnly) { Add-Action -Actions $Actions -Step 'agent-config' -Outcome ("would create {0}" -f $IterationConfigPath) return } $parent = Split-Path -Parent $IterationConfigPath if (-not (Test-Path -LiteralPath $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } [System.IO.File]::WriteAllText($IterationConfigPath, ($managedBlock + [Environment]::NewLine), [System.Text.UTF8Encoding]::new($false)) Add-Action -Actions $Actions -Step 'agent-config' -Outcome ("created {0}" -f $IterationConfigPath) return } $content = Get-Content -LiteralPath $IterationConfigPath -Raw $managedPattern = '(?ms)(\r?\n)?# >>> specrew-managed agents >>>.*?# <<< specrew-managed agents <<<(\r?\n)?' $baseContent = [regex]::Replace($content, $managedPattern, '') $updatedContent = $baseContent.TrimEnd() if ([string]::IsNullOrWhiteSpace($updatedContent)) { $updatedContent = $managedBlock } else { $updatedContent = $updatedContent + [Environment]::NewLine + [Environment]::NewLine + $managedBlock } if ($PreviewOnly) { Add-Action -Actions $Actions -Step 'agent-config' -Outcome ("would update {0}" -f $IterationConfigPath) return } [System.IO.File]::WriteAllText($IterationConfigPath, ($updatedContent + [Environment]::NewLine), [System.Text.UTF8Encoding]::new($false)) Add-Action -Actions $Actions -Step 'agent-config' -Outcome ("updated {0}" -f $IterationConfigPath) } $explicitAgentsValueSpecified = $PSBoundParameters.ContainsKey('Agents') $explicitNoAgentsSpecified = $PSBoundParameters.ContainsKey('NoAgents') $cliArguments = @($CliArgs) for ($cliIndex = 0; $cliIndex -lt $cliArguments.Count; $cliIndex++) { $cliArg = $cliArguments[$cliIndex] if ([string]::IsNullOrWhiteSpace($cliArg)) { continue } switch -Regex ($cliArg) { '^--dry-run$' { $DryRun = $true continue } '^--force$' { $Force = $true continue } '^--help$' { $Help = $true continue } '^--no-agents$' { $NoAgents = $true $explicitNoAgentsSpecified = $true continue } '^--agents=(.+)$' { $Agents = $Matches[1] $explicitAgentsValueSpecified = $true continue } '^--agents$' { if (($cliIndex + 1) -ge $cliArguments.Count -or [string]::IsNullOrWhiteSpace($cliArguments[$cliIndex + 1])) { Write-Error '--agents requires a value.' exit 3 } $cliIndex++ $Agents = $cliArguments[$cliIndex] $explicitAgentsValueSpecified = $true continue } '^--project-path=(.+)$' { $ProjectPath = $Matches[1] continue } '^--project-path$' { if (($cliIndex + 1) -ge $cliArguments.Count -or [string]::IsNullOrWhiteSpace($cliArguments[$cliIndex + 1])) { Write-Error '--project-path requires a value.' exit 3 } $cliIndex++ $ProjectPath = $cliArguments[$cliIndex] continue } '^--speckit-version=(.+)$' { $SpecKitVersion = $Matches[1] continue } '^--speckit-version$' { if (($cliIndex + 1) -ge $cliArguments.Count -or [string]::IsNullOrWhiteSpace($cliArguments[$cliIndex + 1])) { Write-Error '--speckit-version requires a value.' exit 3 } $cliIndex++ $SpecKitVersion = $cliArguments[$cliIndex] continue } '^--squad-version=(.+)$' { $SquadVersion = $Matches[1] continue } '^--squad-version$' { if (($cliIndex + 1) -ge $cliArguments.Count -or [string]::IsNullOrWhiteSpace($cliArguments[$cliIndex + 1])) { Write-Error '--squad-version requires a value.' exit 3 } $cliIndex++ $SquadVersion = $cliArguments[$cliIndex] continue } '^--spec-kit-extension-only$' { $SpecKitExtensionOnly = $true continue } '^--skip-update-check$' { $SkipUpdateCheck = $true continue } default { Write-Error ("Unknown option '{0}'." -f $cliArg) exit 3 } } } if ($explicitAgentsValueSpecified -and $explicitNoAgentsSpecified) { Write-Error "Specify either --agents or --no-agents, not both." exit 3 } if ($Help) { Show-Usage exit 0 } # Pre-flight dependency check Write-Step 'Checking required dependencies' $preFlightCheck = Test-PreFlightDependencies -IncludeOptional if (-not $preFlightCheck.AllOk) { $hasErrors = $preFlightCheck.MissingDeps.Count -gt 0 -or ($preFlightCheck.OutdatedDeps | Where-Object { $_.Tool -ne 'gh' }).Count -gt 0 if ($preFlightCheck.MissingDeps.Count -gt 0) { Write-Host '' Write-Host 'Missing required dependencies:' -ForegroundColor Red Write-Host '' foreach ($dep in $preFlightCheck.MissingDeps) { if ($dep.Tool -eq 'gh') { Write-Host (" [{0}] {1} (optional but recommended)" -f $dep.Platform, $dep.Tool) -ForegroundColor Yellow Write-Host (" {0}" -f $dep.InstallHint) -ForegroundColor DarkGray } else { Write-Host (" [{0}] {1} {2} (required: {3})" -f $dep.Platform, $dep.Tool, $dep.Current, $dep.Required) -ForegroundColor Red Write-Host (" {0}" -f $dep.InstallHint) -ForegroundColor DarkGray } Write-Host '' } } if ($preFlightCheck.OutdatedDeps.Count -gt 0) { Write-Host '' Write-Host 'Outdated dependencies:' -ForegroundColor Yellow Write-Host '' foreach ($dep in $preFlightCheck.OutdatedDeps) { Write-Host (" [{0}] {1} {2} (required: {3})" -f $dep.Platform, $dep.Tool, $dep.Current, $dep.Required) -ForegroundColor Yellow Write-Host (" {0}" -f $dep.InstallHint) -ForegroundColor DarkGray Write-Host '' } } if ($hasErrors) { Write-Host 'Install all required dependencies before running specrew init.' -ForegroundColor Red exit 4 } else { Write-Host 'All required dependencies are installed.' -ForegroundColor Green } } else { Write-Host 'All required dependencies are installed.' -ForegroundColor Green } Write-Host '' $resolvedProjectPath = Resolve-ProjectPath -Path $ProjectPath $executionLayout = Get-SpecrewExecutionLayout $repoRoot = $executionLayout.RootPath $validateVersionsScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\validate-versions.ps1' $deploySpeckitExtensionScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\deploy-speckit-extension.ps1' $deploySquadRuntimeScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\deploy-squad-runtime.ps1' $scaffoldGovernanceScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\scaffold-governance.ps1' $specrewExtensionManifestPath = Join-Path $repoRoot 'extensions\specrew-speckit\extension.yml' $actions = [System.Collections.ArrayList]::new() if (-not (Test-Path -LiteralPath $resolvedProjectPath)) { if ($DryRun) { Add-Action -Actions $actions -Step 'project-path' -Outcome "would create $resolvedProjectPath" } else { New-Item -Path $resolvedProjectPath -ItemType Directory -Force | Out-Null Add-Action -Actions $actions -Step 'project-path' -Outcome "created $resolvedProjectPath" } } $existingEntries = @(Get-ChildItem -Path $resolvedProjectPath -Force -ErrorAction SilentlyContinue) $blockingEntries = @($existingEntries | Where-Object { $_.Name -ne '.git' }) $hadSpecify = Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.specify') $hadSquad = Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.squad') $hadGitHub = Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.github') $hadSpecifyContent = $hadSpecify -and ((@( Get-ChildItem -LiteralPath (Join-Path $resolvedProjectPath '.specify') -Force -ErrorAction SilentlyContinue ).Count) -gt 0) $hadSquadContent = $hadSquad -and ((@( Get-ChildItem -LiteralPath (Join-Path $resolvedProjectPath '.squad') -Force -ErrorAction SilentlyContinue ).Count) -gt 0) $hadGitHubContent = $hadGitHub -and ((@( Get-ChildItem -LiteralPath (Join-Path $resolvedProjectPath '.github') -Force -ErrorAction SilentlyContinue ).Count) -gt 0) $hasSpecrewConfig = Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.specrew\config.yml') $alreadyBootstrapped = $hadSpecify -and $hasSpecrewConfig if (-not $SpecKitExtensionOnly) { $alreadyBootstrapped = $alreadyBootstrapped -and $hadSquad -and $hadGitHub } $bootstrapMode = if ($hadSpecify -or $hadSquad) { 'brownfield' } else { 'greenfield' } $shouldInitializeSpecify = -not $hadSpecify $shouldInitializeSquad = -not $hadSquad $shouldForceSpecifyInit = $Force -or ($blockingEntries.Count -eq 0) $specifySurfaceReady = $hadSpecify -or $shouldInitializeSpecify $squadSurfaceReady = $hadSquad -or $shouldInitializeSquad if ($blockingEntries.Count -gt 0 -and -not $Force -and -not $hadSpecify -and -not $hadSquad) { Write-Error "Target directory '$resolvedProjectPath' is not empty. Re-run with -Force to allow bootstrap into a populated workspace." exit 3 } if ($alreadyBootstrapped -and -not $Force) { Write-Step 'Checking idempotent bootstrap state' Add-Action -Actions $actions -Step 'specify-init' -Outcome 'preserved existing .specify' if (-not $SpecKitExtensionOnly) { Add-Action -Actions $actions -Step 'squad-init' -Outcome 'preserved existing .squad' } Add-Action -Actions $actions -Step 'template-source' -Outcome ("{0}: {1}" -f $executionLayout.Mode, $executionLayout.TemplateRoot) Add-Action -Actions $actions -Step 'template-copy' -Outcome 'preserved existing .specify; re-run with -Force to refresh bundled templates' if (-not $SpecKitExtensionOnly) { Add-Action -Actions $actions -Step 'template-copy' -Outcome 'preserved existing .squad; re-run with -Force to refresh bundled templates' Add-Action -Actions $actions -Step 'template-copy' -Outcome 'preserved existing .github; re-run with -Force to refresh bundled templates' } if ($DryRun) { Add-Action -Actions $actions -Step 'bootstrap-validation' -Outcome 'would validate .specify templates, .squad agents, .github workflows, and .github/agents/squad.agent.md' } else { $bootstrapValidation = Test-BootstrappedProjectState -ProjectPath $resolvedProjectPath -SpecKitExtensionOnly:$SpecKitExtensionOnly if (-not $bootstrapValidation.Succeeded) { foreach ($failure in $bootstrapValidation.Failures) { Write-Error $failure -ErrorAction Continue } exit 1 } $skillCatalogState = if (-not $SpecKitExtensionOnly) { Get-SpecrewSkillCatalogState -ProjectPath $resolvedProjectPath } else { [pscustomobject]@{ HasMissingRoots = $false; MissingRoots = @() } } if ($skillCatalogState.HasMissingRoots) { Add-Action -Actions $actions -Step 'bootstrap-validation' -Outcome 'validated core bootstrap surfaces; skill catalog deployment gap detected' Add-Action -Actions $actions -Step 'skill-catalog-gap' -Outcome (Format-SpecrewSkillCatalogRoots -Roots $skillCatalogState.MissingRoots) Write-Host ("Specrew is already bootstrapped in '{0}', but skill catalog directories are missing. Repairing deployment gap." -f $resolvedProjectPath) -ForegroundColor Yellow $skillCatalogRepair = Invoke-SpecrewSkillCatalogRepair -ProjectPath $resolvedProjectPath -DeploymentScriptPath $deploySquadRuntimeScript -DryRun:$DryRun foreach ($deploymentAction in @($skillCatalogRepair.Actions)) { if ($deploymentAction.PSObject.Properties['Action'] -and $deploymentAction.PSObject.Properties['Path']) { Add-Action -Actions $actions -Step 'squad-runtime' -Outcome ("{0}: {1}" -f $deploymentAction.Action, $deploymentAction.Path) } } if ($skillCatalogRepair.AfterState.HasMissingRoots) { foreach ($missingRoot in @($skillCatalogRepair.AfterState.MissingRoots)) { Write-Error ("Missing required skill catalog directory after repair: {0}" -f $missingRoot.Path) -ErrorAction Continue } exit 1 } Add-Action -Actions $actions -Step 'slash-surface' -Outcome 'repaired /specrew skill catalog across .claude/skills, .github/skills, and .agents/skills' } else { Add-Action -Actions $actions -Step 'bootstrap-validation' -Outcome 'validated .specify templates, .squad agents, .github workflows, and .github/agents/squad.agent.md' Write-Host ("Specrew is already bootstrapped in '{0}'. Re-run with -Force to refresh bundled templates." -f $resolvedProjectPath) -ForegroundColor Yellow } } Write-BootstrapSummary -Actions $actions -DryRunMode:$DryRun -ProjectPath $resolvedProjectPath -ShowGuidance:$false exit 0 } if ($bootstrapMode -eq 'brownfield') { Write-Step 'Running brownfield merge analysis' $brownfieldMergeScript = Join-Path $repoRoot 'extensions\specrew-speckit\scripts\brownfield-merge.ps1' $brownfieldReportJson = & $brownfieldMergeScript ` -ProjectPath $resolvedProjectPath ` -PassThru if ($null -eq $brownfieldReportJson) { Write-Error 'Brownfield merge analysis failed to produce a report.' exit 5 } $brownfieldReport = $brownfieldReportJson | ConvertFrom-Json if ($DryRun) { $timestamp = [datetime]::UtcNow.ToString('yyyyMMddTHHmmss') $dryRunArtifactPath = Join-Path $resolvedProjectPath ".specrew\bootstrap-dry-run-${timestamp}.md" $dryRunContent = @( "# Bootstrap Dry-Run Report" "" "**Generated**: $([datetime]::UtcNow.ToString('yyyy-MM-dd HH:mm:ss')) UTC" "**Project**: $resolvedProjectPath" "**Mode**: brownfield" "**Status**: $($brownfieldReport.Status)" "" "## Brownfield Analysis" "" "- Preserved specs: $($brownfieldReport.PreservedSpecs.Count)" "- Preserved roles: $($brownfieldReport.PreservedRoles.Count)" "- Preserved ceremonies: $($brownfieldReport.PreservedCeremonies.Count)" "- Role conflicts: $($brownfieldReport.RoleConflicts.Count)" "- Ceremony conflicts: $($brownfieldReport.CeremonyConflicts.Count)" "- Mergeable roles: $($brownfieldReport.MergeableRoles.Count)" "- Mergeable ceremonies: $($brownfieldReport.MergeableCeremonies.Count)" "" ) if ($brownfieldReport.Conflicts.Count -gt 0) { $dryRunContent += "## Conflicts" $dryRunContent += "" foreach ($conflict in $brownfieldReport.Conflicts) { $dryRunContent += "### $($conflict.Type)" $dryRunContent += "" $dryRunContent += "**Description**: $($conflict.Description)" $dryRunContent += "" $dryRunContent += "**Resolution**: $($conflict.Resolution)" $dryRunContent += "" } } if ($brownfieldReport.Warnings.Count -gt 0) { $dryRunContent += "## Warnings" $dryRunContent += "" foreach ($warning in $brownfieldReport.Warnings) { $dryRunContent += "### $($warning.Type)" $dryRunContent += "" $dryRunContent += "**Description**: $($warning.Description)" $dryRunContent += "" $dryRunContent += "**Resolution**: $($warning.Resolution)" $dryRunContent += "" } } $dryRunContent += "## Planned Actions" $dryRunContent += "" $dryRunContent += "The following actions would be performed during actual bootstrap:" $dryRunContent += "" $dryRunContent += "1. Preserve existing specs: $($brownfieldReport.PreservedSpecs -join ', ')" if ($brownfieldReport.MergeableRoles.Count -gt 0) { $dryRunContent += "2. Merge baseline roles: $($brownfieldReport.MergeableRoles -join ', ')" } if ($brownfieldReport.MergeableCeremonies.Count -gt 0) { $dryRunContent += "3. Merge ceremonies: $($brownfieldReport.MergeableCeremonies -join ', ')" } $dryRunContent += "" $parentDir = Split-Path -Parent $dryRunArtifactPath if (-not (Test-Path -LiteralPath $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null } [System.IO.File]::WriteAllText($dryRunArtifactPath, ($dryRunContent -join [Environment]::NewLine), [System.Text.UTF8Encoding]::new($false)) Write-Host "Dry-run report written to: $dryRunArtifactPath" -ForegroundColor Cyan } if ($brownfieldReport.Conflicts.Count -gt 0) { Write-Host 'Brownfield merge conflicts detected:' -ForegroundColor Red foreach ($conflict in $brownfieldReport.Conflicts) { Write-Host " - [$($conflict.Type)] $($conflict.Description)" -ForegroundColor Red Write-Host " Resolution: $($conflict.Resolution)" -ForegroundColor Yellow } Write-Host '' Write-Host 'Bootstrap cannot proceed until conflicts are resolved. Run with --dry-run to generate a detailed report, review conflicts, then manually merge or rename conflicting roles/ceremonies before re-running bootstrap.' -ForegroundColor Red exit 5 } if ($brownfieldReport.Warnings.Count -gt 0) { Write-Host 'Brownfield merge warnings:' -ForegroundColor Yellow foreach ($warning in $brownfieldReport.Warnings) { Write-Host " - [$($warning.Type)] $($warning.Description)" -ForegroundColor Yellow Write-Host " Resolution: $($warning.Resolution)" -ForegroundColor Cyan } Write-Host '' } Add-Action -Actions $actions -Step 'brownfield-analysis' -Outcome ("status={0}, conflicts={1}, warnings={2}" -f $brownfieldReport.Status, $brownfieldReport.Conflicts.Count, $brownfieldReport.Warnings.Count) } Write-Step 'Validating platform dependencies' $requiredPlatforms = if ($SpecKitExtensionOnly) { @('Spec Kit') } else { @('Spec Kit', 'Squad') } $versionResults = @( Invoke-VersionValidation -ScriptPath $validateVersionsScript -MinimumSpecKitVersion $SpecKitVersion -MinimumSquadVersion $SquadVersion | Where-Object { $requiredPlatforms -contains $_.Platform } ) $missingDependencies = @($versionResults | Where-Object { -not $_.IsInstalled }) $preInstallFailureExitCode = Resolve-DependencyValidationIssue -Results $versionResults -Actions $actions -PreviewOnly:$DryRun -IncludeMissing:$false -AfterInstallAttempt:$false if ($preInstallFailureExitCode -ne 0 -and -not $DryRun) { exit $preInstallFailureExitCode } foreach ($dependency in $missingDependencies) { Write-Step ("Installing missing dependency: {0}" -f $dependency.Platform) try { Install-MissingDependency -Dependency $dependency -PreviewOnly:$DryRun Add-Action -Actions $actions -Step 'dependency' -Outcome ("{0}: {1}" -f $dependency.Platform, $(if ($DryRun) { 'would install' } else { 'installed' })) } catch { Write-Error $_ exit 4 } } if ($missingDependencies.Count -gt 0 -and -not $DryRun) { $versionResults = @( Invoke-VersionValidation -ScriptPath $validateVersionsScript -MinimumSpecKitVersion $SpecKitVersion -MinimumSquadVersion $SquadVersion | Where-Object { $requiredPlatforms -contains $_.Platform } ) $postInstallFailureExitCode = Resolve-DependencyValidationIssue -Results $versionResults -Actions $actions -PreviewOnly:$false -IncludeMissing:$true -AfterInstallAttempt:$true if ($postInstallFailureExitCode -ne 0) { exit $postInstallFailureExitCode } } $resolvedAgents = @() if (-not $SpecKitExtensionOnly) { Write-Step 'Detecting Copilot runtime and delegated agents' $agentDetection = Get-AgentDetection -WorkingDirectory $repoRoot try { $resolvedAgents = Resolve-AgentSelection -DetectedAgents $agentDetection.Agents -DisableAll:$NoAgents -RequestedAgents $Agents } catch { Write-Error $_ exit 3 } Add-Action -Actions $actions -Step 'agent-detection' -Outcome (Format-AgentSummary -Agents $resolvedAgents) if (-not $agentDetection.AuthContextAvailable) { Write-Host 'GitHub auth context is unavailable in this environment. Continuing without failing bootstrap.' -ForegroundColor Yellow } if (-not $agentDetection.DelegatedMetadataAvailable) { Write-Host 'Delegated-agent metadata is unavailable in this environment. Continuing without failing bootstrap.' -ForegroundColor Yellow } } if ($shouldInitializeSpecify) { Write-Step 'Running specify init' if ($DryRun) { Write-Host ("[dry-run] specify init --here --ai copilot --script ps --ignore-agent-tools{0}" -f $(if ($shouldForceSpecifyInit) { ' --force' } else { '' })) -ForegroundColor Yellow Add-Action -Actions $actions -Step 'specify-init' -Outcome 'would initialize .specify' } else { $specifyArguments = @('init', '--here', '--ai', 'copilot', '--script', 'ps', '--ignore-agent-tools') if ($shouldForceSpecifyInit) { $specifyArguments += '--force' } Write-Step 'Preflighting specify init' $specifyPreflight = Test-SpecifyInitPreflight -ProjectPath $resolvedProjectPath -ArgumentList $specifyArguments -SpecKitVersion $SpecKitVersion if (-not $specifyPreflight.Ready) { Write-Error $specifyPreflight.FailureMessage exit 1 } if ($specifyPreflight.Repaired) { Add-Action -Actions $actions -Step 'dependency' -Outcome ("Spec Kit: {0}" -f $specifyPreflight.RepairOutcome) } $specifyInitResult = Invoke-NativeCommandForOutput -FilePath 'specify' -ArgumentList $specifyArguments -WorkingDirectory $resolvedProjectPath if ($specifyInitResult.ExitCode -ne 0) { $failureSummary = Get-FirstNonEmptyOutputLine -OutputLines $specifyInitResult.Output if ($failureSummary) { Write-Error ("specify init failed after preflight: {0}" -f $failureSummary) } else { Write-Error 'specify init failed after preflight with no diagnostic output.' } exit 1 } Add-Action -Actions $actions -Step 'specify-init' -Outcome 'initialized .specify' } } else { if ($hadSpecify) { Add-Action -Actions $actions -Step 'specify-init' -Outcome 'preserved existing .specify' } else { Add-Action -Actions $actions -Step 'specify-init' -Outcome 'skipped: brownfield bootstrap does not initialize missing .specify' } } if (-not $SpecKitExtensionOnly -and $shouldInitializeSquad) { Write-Step 'Running squad init' $squadInitPlan = Get-SquadInitPlan -ProbeRoot $repoRoot if ($squadInitPlan.SupportsNonInteractive) { if ($DryRun) { Write-Host ("[dry-run] squad {0}" -f ($squadInitPlan.ArgumentList -join ' ')) -ForegroundColor Yellow Add-Action -Actions $actions -Step 'squad-init' -Outcome 'would initialize .squad via squad init --non-interactive' } else { Invoke-NativeCommand -FilePath 'squad' -ArgumentList $squadInitPlan.ArgumentList -WorkingDirectory $resolvedProjectPath Add-Action -Actions $actions -Step 'squad-init' -Outcome 'initialized .squad via squad init --non-interactive' } } else { Write-Step 'Scaffolding .squad fallback' Write-Host '[info] squad init --non-interactive is unavailable; using direct .squad scaffold fallback.' -ForegroundColor Yellow Initialize-SquadFallbackScaffold -ProjectPath $resolvedProjectPath -PreviewOnly:$DryRun Add-Action -Actions $actions -Step 'squad-init' -Outcome ($(if ($DryRun) { 'would initialize .squad via fallback scaffold' } else { 'initialized .squad via fallback scaffold' })) } } else { if (-not $SpecKitExtensionOnly -and $hadSquad) { Add-Action -Actions $actions -Step 'squad-init' -Outcome 'preserved existing .squad' } elseif (-not $SpecKitExtensionOnly) { Add-Action -Actions $actions -Step 'squad-init' -Outcome 'skipped: brownfield bootstrap does not initialize missing .squad' } } Write-Step 'Deploying Specrew Spec Kit extension' if ($specifySurfaceReady) { $specKitDeploymentResult = Invoke-SpecKitExtensionDeployment ` -ProjectPath $resolvedProjectPath ` -RepoRoot $repoRoot ` -FallbackScriptPath $deploySpeckitExtensionScript ` -PreviewOnly:$DryRun $specKitDeploymentAction = if ($null -ne $specKitDeploymentResult -and $specKitDeploymentResult.PSObject.Properties['Action']) { [string]$specKitDeploymentResult.Action } else { if ($DryRun) { 'would-install' } else { 'installed' } } $specKitDeploymentPath = if ($null -ne $specKitDeploymentResult -and $specKitDeploymentResult.PSObject.Properties['Path']) { [string]$specKitDeploymentResult.Path } else { Join-Path $resolvedProjectPath '.specify\extensions\specrew-speckit' } Add-Action -Actions $actions -Step 'spec-kit-extension' -Outcome ("{0}: {1}" -f $specKitDeploymentAction, $specKitDeploymentPath) } else { Add-Action -Actions $actions -Step 'spec-kit-extension' -Outcome 'skipped: .specify is absent in brownfield workspace' } Write-Step 'Deploying bundled project templates' Invoke-BundledTemplateDeployment ` -ExecutionLayout $executionLayout ` -ProjectPath $resolvedProjectPath ` -ForceRefresh:$Force ` -SpecKitReady:$specifySurfaceReady ` -SquadReady:$squadSurfaceReady ` -HadSpecify:$hadSpecifyContent ` -HadSquad:$hadSquadContent ` -HadGitHub:$hadGitHubContent ` -SpecKitExtensionOnly:$SpecKitExtensionOnly ` -Actions $actions ` -PreviewOnly:$DryRun if (-not $SpecKitExtensionOnly) { $resolvedSpecKitVersion = (($versionResults | Where-Object { $_.Platform -eq 'Spec Kit' } | Select-Object -First 1).Version) if ([string]::IsNullOrWhiteSpace($resolvedSpecKitVersion)) { $resolvedSpecKitVersion = $SpecKitVersion } $resolvedSquadVersion = (($versionResults | Where-Object { $_.Platform -eq 'Squad' } | Select-Object -First 1).Version) if ([string]::IsNullOrWhiteSpace($resolvedSquadVersion)) { $resolvedSquadVersion = $SquadVersion } $specrewManifestContent = Get-Content -LiteralPath $specrewExtensionManifestPath -Raw $specrewVersionMatch = [regex]::Match($specrewManifestContent, '(?m)^\s*version:\s*"?(?<version>[^"\r\n]+)') $resolvedSpecrewVersion = if ($specrewVersionMatch.Success) { $specrewVersionMatch.Groups['version'].Value.Trim() } else { '0.1.0-dev' } Write-Step 'Scaffolding downstream governance' $governanceActions = @( & $scaffoldGovernanceScript ` -ProjectPath $resolvedProjectPath ` -SpecrewVersion $resolvedSpecrewVersion ` -SpecKitVersion $resolvedSpecKitVersion ` -SquadVersion $resolvedSquadVersion ` -BootstrapMode $bootstrapMode ` -DryRun:$DryRun ` -PassThru ) foreach ($governanceAction in $governanceActions) { Add-Action -Actions $actions -Step 'governance-scaffold' -Outcome ("{0}: {1}" -f $governanceAction.Action, $governanceAction.Path) } Write-Step 'Deploying Squad runtime' $iterationConfigPath = Join-Path $resolvedProjectPath '.specrew\iteration-config.yml' Set-IterationConfigAgents -IterationConfigPath $iterationConfigPath -Agents $resolvedAgents -Actions $actions -PreviewOnly:$DryRun if ($squadSurfaceReady) { $squadDeploymentActions = @( & $deploySquadRuntimeScript ` -ProjectPath $resolvedProjectPath ` -DryRun:$DryRun ` -PassThru ) foreach ($deploymentAction in $squadDeploymentActions) { Add-Action -Actions $actions -Step 'squad-runtime' -Outcome ("{0}: {1}" -f $deploymentAction.Action, $deploymentAction.Path) } Add-Action -Actions $actions -Step 'slash-surface' -Outcome 'provisioned /specrew-where, /specrew-status, /specrew-update, /specrew-team, /specrew-review, /specrew-help, /specrew-version across .claude/skills, .github/skills, and .agents/skills' } else { Add-Action -Actions $actions -Step 'squad-runtime' -Outcome 'skipped: .squad is absent in brownfield workspace' } } Write-Step 'Seeding canonical Crew team at .specrew/team/' # Proposal 108 Slice 9: .specrew/team/agents/<role>.md is the SINGLE SOURCE OF TRUTH for # the 5-agent baseline + user-added specialists. Each host's Install-<Kind>CrewRuntime reads # from here at `specrew start --host <kind>` time and translates to the host's native location. if (Get-Command Initialize-SpecrewTeam -ErrorAction SilentlyContinue) { $teamSeed = Initialize-SpecrewTeam -ProjectPath $resolvedProjectPath -DryRun:$DryRun foreach ($action in $teamSeed.Actions) { $stepName = if ($action.Action -eq 'preserved') { 'team-canonical-preserved' } else { 'team-canonical' } Add-Action -Actions $actions -Step $stepName -Outcome ("{0}: {1}" -f $action.Action, $action.Path) } } Write-Step 'Configuring git for boundary-commit hygiene' # F-040 dogfooding fix (calc-v2 + tip-calc 2026-05-23): when the project is in a git repo, # silence the LF/CRLF warning wall that otherwise dumps 150+ lines on the user during # `git add` for the very first boundary commit. Use `core.safecrlf=false` (warnings off) + # `core.autocrlf=true` on Windows / `input` on POSIX (right normalization). Both are scoped # to THIS repo only via `git config --local`, so we don't touch the user's global config. $gitRepoCheck = Test-Path -LiteralPath (Join-Path $resolvedProjectPath '.git') -PathType Container if ($gitRepoCheck -and -not $DryRun) { try { & git -C $resolvedProjectPath config --local core.safecrlf false 2>$null $global:LASTEXITCODE = 0 $autocrlfValue = if ($IsWindows -or $env:OS -eq 'Windows_NT') { 'true' } else { 'input' } & git -C $resolvedProjectPath config --local core.autocrlf $autocrlfValue 2>$null $global:LASTEXITCODE = 0 Add-Action -Actions $actions -Step 'git-config' -Outcome ("set local core.safecrlf=false + core.autocrlf={0} to suppress harmless CRLF warnings on first commit" -f $autocrlfValue) } catch { Add-Action -Actions $actions -Step 'git-config' -Outcome ("skipped (git config failed: {0})" -f $_.Exception.Message) } } elseif ($gitRepoCheck -and $DryRun) { Add-Action -Actions $actions -Step 'git-config' -Outcome 'would set local core.safecrlf + core.autocrlf to suppress CRLF warnings' } else { Add-Action -Actions $actions -Step 'git-config' -Outcome 'skipped (no .git directory found; this is not a git repo yet)' } Write-Step 'Validating bootstrapped project state' if ($DryRun) { Add-Action -Actions $actions -Step 'bootstrap-validation' -Outcome 'would validate .specify templates, .squad agents, .github workflows, and .github/agents/squad.agent.md' } else { $bootstrapValidation = Test-BootstrappedProjectState -ProjectPath $resolvedProjectPath -SpecKitExtensionOnly:$SpecKitExtensionOnly if (-not $bootstrapValidation.Succeeded) { foreach ($failure in $bootstrapValidation.Failures) { Write-Error $failure -ErrorAction Continue } exit 1 } Add-Action -Actions $actions -Step 'bootstrap-validation' -Outcome 'validated .specify templates, .squad agents, .github workflows, and .github/agents/squad.agent.md' if (-not $SpecKitExtensionOnly) { $finalSkillCatalogState = Get-SpecrewSkillCatalogState -ProjectPath $resolvedProjectPath if ($finalSkillCatalogState.HasMissingRoots) { foreach ($missingRoot in @($finalSkillCatalogState.MissingRoots)) { Write-Error ("Missing required skill catalog directory after deployment: {0}" -f $missingRoot.Path) -ErrorAction Continue } exit 1 } Add-Action -Actions $actions -Step 'skill-catalog' -Outcome 'validated .claude/skills, .github/skills, and .agents/skills' } } Write-BootstrapSummary -Actions $actions -DryRunMode:$DryRun -ProjectPath $resolvedProjectPath -ShowGuidance:(-not $SpecKitExtensionOnly -and $squadSurfaceReady) if (-not $DryRun) { $psGalleryUpdateWarning = Get-PSGalleryUpdateWarning -ProjectRoot $resolvedProjectPath -SkipCheck:$SkipUpdateCheck if (-not [string]::IsNullOrWhiteSpace($psGalleryUpdateWarning)) { Write-Output ("WARN: {0}" -f $psGalleryUpdateWarning) } } exit 0 |