diff --git a/.github/workflows/scripts/create-release-packages.ps1 b/.github/workflows/scripts/create-release-packages.ps1 index 912dd00ec..b9498ab73 100644 --- a/.github/workflows/scripts/create-release-packages.ps1 +++ b/.github/workflows/scripts/create-release-packages.ps1 @@ -14,7 +14,7 @@ .PARAMETER Agents Comma or space separated subset of agents to build (default: all) - Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, junie, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, trae, pi, iflow, generic + Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, junie, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, tabnine, agy, vibe, kimi, trae, pi, iflow, forge, generic .PARAMETER Scripts Comma or space separated subset of script types to build (default: both) @@ -73,7 +73,8 @@ function Generate-Commands { [string]$Extension, [string]$ArgFormat, [string]$OutputDir, - [string]$ScriptVariant + [string]$ScriptVariant, + [string]$ExtraStripKey = "" ) New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null @@ -137,7 +138,16 @@ function Generate-Commands { } if ($inFrontmatter) { + # Check for scripts/agent_scripts or extra strip key + $shouldSkip = $false if ($line -match '^(scripts|agent_scripts):$') { + $shouldSkip = $true + } + if (-not [string]::IsNullOrEmpty($ExtraStripKey) -and $line -match "^${ExtraStripKey}:") { + $shouldSkip = $true + } + + if ($shouldSkip) { $skipScripts = $true continue } @@ -156,6 +166,7 @@ function Generate-Commands { # Apply other substitutions $body = $body -replace '\{ARGS\}', $ArgFormat + $body = $body -replace '\$ARGUMENTS', $ArgFormat $body = $body -replace '__AGENT__', $Agent $body = Rewrite-Paths -Content $body @@ -477,6 +488,22 @@ function Build-Variant { $cmdDir = Join-Path $baseDir ".iflow/commands" Generate-Commands -Agent 'iflow' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script } + 'forge' { + $cmdDir = Join-Path $baseDir ".forge/commands" + Generate-Commands -Agent 'forge' -Extension 'md' -ArgFormat '{{parameters}}' -OutputDir $cmdDir -ScriptVariant $Script -ExtraStripKey 'handoffs' + + # Inject name field into frontmatter (forge requires name + description) + $cmdFiles = Get-ChildItem -Path "$cmdDir/*.md" -File -ErrorAction SilentlyContinue + foreach ($cmdFile in $cmdFiles) { + $cmdName = [System.IO.Path]::GetFileNameWithoutExtension($cmdFile.Name) + $content = Get-Content -Path $cmdFile.FullName -Raw + + # Inject name field after first --- + $content = $content -replace '(?m)^---$', "---`nname: $cmdName", 1 + + Set-Content -Path $cmdFile.FullName -Value $content -NoNewline + } + } 'generic' { $cmdDir = Join-Path $baseDir ".speckit/commands" Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script @@ -493,7 +520,7 @@ function Build-Variant { } # Define all agents and scripts -$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'junie', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'trae', 'pi', 'iflow', 'generic') +$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'junie', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'tabnine', 'agy', 'vibe', 'kimi', 'trae', 'pi', 'iflow', 'forge', 'generic') $AllScripts = @('sh', 'ps') function Normalize-List { diff --git a/tests/test_core_pack_scaffold.py b/tests/test_core_pack_scaffold.py index af2da13d5..3e1b8c7f6 100644 --- a/tests/test_core_pack_scaffold.py +++ b/tests/test_core_pack_scaffold.py @@ -466,12 +466,12 @@ def test_markdown_has_frontmatter(agent, scaffolded_sh): def test_forge_name_field_in_frontmatter(scaffolded_sh): """Forge: every command file must have a 'name' field in frontmatter that matches the filename. - + Forge requires both 'name' and 'description' fields in command frontmatter. This test ensures the release script's name injection is working correctly. """ project = scaffolded_sh("forge") - + cmd_dir = _expected_cmd_dir(project, "forge") for f in _list_command_files(cmd_dir, "forge"): content = f.read_text(encoding="utf-8") @@ -482,13 +482,13 @@ def test_forge_name_field_in_frontmatter(scaffolded_sh): assert len(parts) >= 3, f"Incomplete frontmatter in '{f.name}'" fm = yaml.safe_load(parts[1]) assert fm is not None, f"Empty frontmatter in '{f.name}'" - + # Check that 'name' field exists assert "name" in fm, ( f"'name' key missing from frontmatter in '{f.name}' - " f"Forge requires both 'name' and 'description' fields" ) - + # Check that name matches the filename (without extension) expected_name = f.name.removesuffix(".md") actual_name = fm["name"]