Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
* text=auto eol=lf

# Polyglot wrappers - MUST use Unix line endings (LF) to work on Windows
scripts/check-prerequisites text eol=lf
scripts/setup-plan text eol=lf
scripts/create-new-feature text eol=lf
scripts/update-agent-context text eol=lf

# Platform-specific scripts (explicit for clarity)
scripts/bash/*.sh text eol=lf
scripts/powershell/*.ps1 text eol=crlf
18 changes: 9 additions & 9 deletions .github/workflows/scripts/create-release-packages.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,27 @@ function Generate-Commands {
$description = $matches[1]
}

# Extract script command from YAML frontmatter
# Extract script command from scripts: section (both sh and ps now have same path)
$scriptCommand = ""
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
$scriptCommand = $matches[1]
if ($fileContent -match '(?m)^\s*sh:\s*(.+)$') {
$scriptCommand = $matches[1].Trim()
}

if ([string]::IsNullOrEmpty($scriptCommand)) {
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
$scriptCommand = "(Missing script command for $ScriptVariant)"
Write-Warning "No script command found in $($template.Name)"
$scriptCommand = "(Missing script command)"
}
Comment on lines +95 to 104
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generate-Commands now always extracts {SCRIPT} from the sh: frontmatter line, but Build-Variant still copies only one of scripts/bash or scripts/powershell depending on $Script. This combination can produce a ps package whose command files call the polyglot wrapper even though the wrapper’s Unix delegation target isn’t present in that package. Consider either bundling both script directories in both variants when using wrappers, or restoring per-variant extraction.

Copilot uses AI. Check for mistakes.

# Extract agent_script command from YAML frontmatter if present
# Extract agent_script command from agent_scripts: section if present
$agentScriptCommand = ""
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
if ($fileContent -match '(?ms)agent_scripts:.*?^\s*sh:\s*(.+?)$') {
$agentScriptCommand = $matches[1].Trim()
}

# Replace {SCRIPT} placeholder with the script command
# Replace {SCRIPT} placeholder with the actual polyglot wrapper path
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand

# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
# Replace {AGENT_SCRIPT} placeholder if found
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
}
Expand Down
22 changes: 12 additions & 10 deletions .github/workflows/scripts/create-release-packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,35 +49,37 @@ generate_commands() {
# Normalize line endings
file_content=$(tr -d '\r' < "$template")

# Extract description and script command from YAML frontmatter
# Extract description from YAML frontmatter
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')

# Extract script command from scripts: section (both sh and ps now have same path)
script_command=$(printf '%s\n' "$file_content" | awk '/^[[:space:]]*sh:[[:space:]]*/ {sub(/^[[:space:]]*sh:[[:space:]]*/, ""); print; exit}')

if [[ -z $script_command ]]; then
echo "Warning: no script command found for $script_variant in $template" >&2
script_command="(Missing script command for $script_variant)"
echo "Warning: no script command found in $template" >&2
script_command="(Missing script command)"
fi
Comment on lines +55 to 61
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These packages still ship only one variant directory (scripts/bash for sh, scripts/powershell for ps), but the generated command files now always expand {SCRIPT} from the sh: entry. That means the ps package’s commands will reference the polyglot wrapper even though the wrapper’s Unix path points at scripts/bash/... (which isn’t included in the ps package). Either include both bash/ and powershell/ in both packages when using polyglot wrappers, or keep variant-specific script expansion.

See below for a potential fix:

    # Extract script command from scripts: section, using the current script_variant (sh or ps)
    script_command=$(printf '%s\n' "$file_content" | awk -v variant="$script_variant" '
      {
        line = $0
        # Strip leading whitespace for matching the key
        gsub(/^[[:space:]]*/, "", line)
        # Look for a line starting with "<variant>:"
        if (index(line, variant ":") == 1) {
          # Remove leading whitespace and "<variant>:" plus any following spaces
          sub(/^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*/, "", $0)
          print
          exit
        }
      }
    ')
    
    if [[ -z $script_command ]]; then
      echo "Warning: no script command found in $template for variant '$script_variant'" >&2
      script_command="(Missing script command)"
    fi
    
    # Extract agent_script command from agent_scripts: section if present, using the current script_variant
    agent_script_command=$(printf '%s\n' "$file_content" | awk -v variant="$script_variant" '
      /^agent_scripts:$/ { in_agent_scripts=1; next }
      in_agent_scripts {
        line = $0
        # End of agent_scripts: block when we hit a non-indented key
        if (line ~ /^[a-zA-Z]/) {
          in_agent_scripts=0
          next
        }
        # Within the block, look for a line whose trimmed form starts with "<variant>:"
        gsub(/^[[:space:]]*/, "", line)
        if (index(line, variant ":") == 1) {
          sub(/^[[:space:]]*[a-zA-Z_][a-zA-Z0-9_]*:[[:space:]]*/, "", $0)
          print
          exit
        }
      }

Copilot uses AI. Check for mistakes.

# Extract agent_script command from YAML frontmatter if present
# Extract agent_script command from agent_scripts: section if present
agent_script_command=$(printf '%s\n' "$file_content" | awk '
/^agent_scripts:$/ { in_agent_scripts=1; next }
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
in_agent_scripts && /^[[:space:]]*sh:[[:space:]]*/ {
sub(/^[[:space:]]*sh:[[:space:]]*/, "")
print
exit
}
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
')

# Replace {SCRIPT} placeholder with the script command
# Replace {SCRIPT} placeholder with the actual polyglot wrapper path
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")

# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
# Replace {AGENT_SCRIPT} placeholder if found
if [[ -n $agent_script_command ]]; then
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
fi

# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
# Remove the scripts: and agent_scripts: sections from frontmatter
body=$(printf '%s\n' "$body" | awk '
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
Expand Down
157 changes: 157 additions & 0 deletions .github/workflows/test-polyglot-wrappers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
name: Test Polyglot Wrappers

permissions:
contents: read

on:
push:
branches: ["main"]
paths:
- 'scripts/**'
- 'templates/commands/**'
- '.gitattributes'
- 'tests/test-polyglot-wrappers.sh'
- '.github/workflows/test-polyglot-wrappers.yml'
pull_request:
paths:
- 'scripts/**'
- 'templates/commands/**'
- '.gitattributes'
- 'tests/test-polyglot-wrappers.sh'
- '.github/workflows/test-polyglot-wrappers.yml'

jobs:
test-unix:
name: Test on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set execute permissions
run: |
chmod +x scripts/check-prerequisites
chmod +x scripts/setup-plan
chmod +x scripts/create-new-feature
chmod +x scripts/update-agent-context
chmod +x scripts/bash/*.sh
chmod +x tests/test-polyglot-wrappers.sh

- name: Run polyglot wrapper tests
run: tests/test-polyglot-wrappers.sh

- name: Test wrapper execution (check-prerequisites)
run: |
echo "Testing check-prerequisites wrapper..."
scripts/check-prerequisites --help

- name: Test wrapper execution (setup-plan)
run: |
echo "Testing setup-plan wrapper..."
scripts/setup-plan --help

- name: Test wrapper execution (create-new-feature)
run: |
echo "Testing create-new-feature wrapper..."
scripts/create-new-feature --help

- name: Verify line endings (LF)
run: |
echo "Verifying polyglot wrappers have LF line endings..."
for wrapper in check-prerequisites setup-plan create-new-feature update-agent-context; do
if file "scripts/$wrapper" | grep -q "CRLF"; then
echo "ERROR: scripts/$wrapper has CRLF line endings (should be LF)"
exit 1
fi
echo "✓ scripts/$wrapper has correct LF line endings"
done

test-windows:
name: Test on Windows
runs-on: windows-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Test wrapper execution via Git Bash (check-prerequisites)
shell: bash
run: |
echo "Testing check-prerequisites wrapper via Git Bash..."
chmod +x scripts/check-prerequisites
chmod +x scripts/bash/*.sh
scripts/check-prerequisites --help

- name: Test wrapper execution via Git Bash (setup-plan)
shell: bash
run: |
echo "Testing setup-plan wrapper via Git Bash..."
chmod +x scripts/setup-plan
scripts/setup-plan --help

- name: Test wrapper execution via cmd.exe (check-prerequisites)
shell: cmd
run: |
echo Testing check-prerequisites wrapper via cmd.exe...
scripts\check-prerequisites --help

- name: Test wrapper execution via PowerShell (check-prerequisites)
shell: pwsh
run: |
Write-Host "Testing check-prerequisites wrapper via PowerShell..."
& "scripts/check-prerequisites" --help
Comment on lines +100 to +107
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, PowerShell won’t reliably execute an extensionless wrapper as a script/application when invoked by path (& "scripts/check-prerequisites"). Consider providing a Windows-invokable shim (e.g., .cmd) or changing the invocation to pwsh -File scripts/powershell/check-prerequisites.ps1 ... in this workflow.

This issue also appears on line 97 of the same file.

Suggested change
echo Testing check-prerequisites wrapper via cmd.exe...
scripts\check-prerequisites --help
- name: Test wrapper execution via PowerShell (check-prerequisites)
shell: pwsh
run: |
Write-Host "Testing check-prerequisites wrapper via PowerShell..."
& "scripts/check-prerequisites" --help
echo Testing check-prerequisites script via cmd.exe using PowerShell...
pwsh -File scripts/powershell/check-prerequisites.ps1 --help
- name: Test wrapper execution via PowerShell (check-prerequisites)
shell: pwsh
run: |
Write-Host "Testing check-prerequisites script via PowerShell..."
& "scripts/powershell/check-prerequisites.ps1" --help

Copilot uses AI. Check for mistakes.

- name: Verify line endings (LF) on Windows
shell: bash
run: |
echo "Verifying polyglot wrappers have LF line endings on Windows..."
for wrapper in check-prerequisites setup-plan create-new-feature update-agent-context; do
# Check if file has CRLF (should not)
if file "scripts/$wrapper" 2>/dev/null | grep -q "CRLF"; then
echo "ERROR: scripts/$wrapper has CRLF line endings (should be LF even on Windows)"
exit 1
fi
echo "✓ scripts/$wrapper has correct LF line endings"
done

test-command-templates:
name: Verify Command Templates
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Check for platform-specific script references
run: |
echo "Checking command templates for old platform-specific references..."

# These should NOT exist in templates anymore
if grep -r "scripts/bash/" templates/commands/ 2>/dev/null; then
echo "ERROR: Found old bash-specific script references in templates"
exit 1
fi

if grep -r "scripts/powershell/" templates/commands/ 2>/dev/null; then
echo "ERROR: Found old PowerShell-specific script references in templates"
exit 1
fi

echo "✓ No platform-specific script references found in templates"

- name: Verify polyglot wrapper references
run: |
echo "Verifying command templates use polyglot wrappers..."

# Check that templates reference the polyglot wrappers
if ! grep -q "scripts/check-prerequisites" templates/commands/*.md; then
echo "ERROR: No references to polyglot wrapper 'scripts/check-prerequisites' found"
exit 1
fi

echo "✓ Command templates correctly reference polyglot wrappers"
117 changes: 115 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,117 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their

- Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`.

## Polyglot Wrapper Scripts

To eliminate platform-specific script references and merge conflicts in mixed-OS teams, Spec Kit uses polyglot wrapper scripts that work on both Unix and Windows.

### How Polyglot Wrappers Work

Wrapper scripts (e.g., `scripts/check-prerequisites`) have no file extension and use a special pattern:

```bash
#!/usr/bin/env bash
# 2>nul & @echo off & goto :batch

# ===== UNIX SECTION =====
"$(dirname "$0")/bash/script-name.sh" "$@"
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the example wrapper, the Unix section line is missing the bash invocation used in the real wrappers (it currently shows just a quoted path). As written, copying this snippet would fail; update it to match the actual wrapper pattern (invoke bash and keep quoting consistent).

Suggested change
"$(dirname "$0")/bash/script-name.sh" "$@"
bash "$(dirname "$0")/bash/script-name.sh" "$@"

Copilot uses AI. Check for mistakes.
exit $?

:batch
@REM ===== WINDOWS SECTION =====
@powershell -ExecutionPolicy Bypass -File "%~dp0powershell\script-name.ps1" %*
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows example uses @powershell ..., but the repository’s PowerShell scripts are pwsh-targeted (e.g., scripts/powershell/*.ps1 start with #!/usr/bin/env pwsh). Using powershell here is likely to mislead users into running scripts with Windows PowerShell 5.1, which may fail on the shebang line; consider updating the example to use pwsh instead.

Suggested change
@powershell -ExecutionPolicy Bypass -File "%~dp0powershell\script-name.ps1" %*
@pwsh -ExecutionPolicy Bypass -File "%~dp0powershell\script-name.ps1" %*

Copilot uses AI. Check for mistakes.
```

**On Unix/macOS:**
- Shebang (`#!/usr/bin/env bash`) executes bash
- `#` lines are comments
- Runs Unix section, exits before `:batch`

**On Windows cmd.exe:**
- `#` fails silently (redirected to `nul`)
- `goto :batch` jumps to Windows section
- Executes PowerShell script

**On Git Bash (Windows):**
- Behaves like Unix (shebang works)

### Benefits

✅ **Single reference** - All agents use same path: `scripts/check-prerequisites`
✅ **No merge conflicts** - Same path works on all platforms
✅ **Team harmony** - Mixed OS teams use identical commands
✅ **Simpler docs** - No platform-specific instructions
✅ **Backward compatible** - Original scripts still work

### Implementation Details

**Polyglot wrappers:**
- `scripts/check-prerequisites`
- `scripts/setup-plan`
- `scripts/create-new-feature`
- `scripts/update-agent-context`

**Platform-specific implementations:**
- `scripts/bash/*.sh` (Unix/macOS)
- `scripts/powershell/*.ps1` (Windows)

**Argument conversion:**
- Windows wrappers automatically convert kebab-case to PowerShell PascalCase
- `--json` → `-Json`, `--require-tasks` → `-RequireTasks`, etc.
- Uses helper script: `scripts/powershell/Convert-KebabToPascal.ps1`
- Escape hatch: Use `--` separator to pass remaining args unchanged
- Debug mode: Set `DEBUG_WRAPPER=1` to see conversions

**Git configuration:**
- Polyglot wrappers MUST use LF line endings (enforced in `.gitattributes`)
- Execute permissions set automatically by `specify init`

### Command Template Architecture

Command templates (`templates/commands/*.md`) use a `scripts:` section during package generation:

```yaml
---
description: "Command description"
scripts:
sh: scripts/check-prerequisites --json
ps: scripts/check-prerequisites --json
---

## Outline

1. **Setup**: Run `{SCRIPT}` from repo root...
```

**How it works:**

1. **Template files** contain both `sh:` and `ps:` entries with identical paths (pointing to polyglot wrappers)
2. **During package generation**, the `{SCRIPT}` placeholder is replaced with the actual polyglot wrapper path
3. **The `scripts:` section is removed** from the generated command files
4. **Generated files contain direct paths** like `.specify/scripts/setup-plan --json`
5. **Polyglot wrappers** handle platform detection internally and delegate to the correct implementation

**Critical design principle:**

The `{SCRIPT}` placeholder is **expanded during package generation** to create platform-agnostic command files with direct, concrete paths. This ensures:

- ✅ Generated command files have direct executable paths
- ✅ No frontmatter parsing required by AI agents
- ✅ LLMs simply execute the literal command as written
- ✅ Same command files work on Windows, macOS, and Linux via polyglot wrappers
- ✅ No merge conflicts when teams use different operating systems

**Example flow:**

1. User on Windows runs `specify init --ai bob`
2. Package generation expands `{SCRIPT}` to `.specify/scripts/setup-plan --json`
3. Bob command file contains: `1. **Setup**: Run .specify/scripts/setup-plan --json from repo root...`
4. Bob executes the literal path `.specify/scripts/setup-plan`
5. Polyglot wrapper detects Windows and runs `scripts/powershell/setup-plan.ps1`
6. User commits file - it works identically on macOS teammate's machine (polyglot wrapper detects macOS and runs bash version)

This architecture allows 19+ AI agents to share the same command templates while providing simple, direct execution paths that work across all platforms.

## Adding New Agent Support

This section explains how to add support for new AI agents/assistants to the Specify CLI. Use this guide as a reference when integrating new AI tools into the Spec-Driven Development workflow.
Expand Down Expand Up @@ -139,7 +250,7 @@ gh release create "$VERSION" \

#### 5. Update Agent Context Scripts

##### Bash script (`scripts/bash/update-agent-context.sh`)
##### Bash script (`scripts/update-agent-context`)

Add file variable:

Expand All @@ -161,7 +272,9 @@ case "$AGENT_TYPE" in
esac
```

##### PowerShell script (`scripts/powershell/update-agent-context.ps1`)
**Note:** The polyglot wrapper `scripts/update-agent-context` automatically delegates to the appropriate platform-specific implementation (`scripts/bash/update-agent-context.sh` or `scripts/powershell/update-agent-context.ps1`).

##### PowerShell script (`scripts/update-agent-context`)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section label says “PowerShell script (scripts/update-agent-context)”, but the platform-specific implementation lives under scripts/powershell/update-agent-context.ps1 (the wrapper is scripts/update-agent-context). The heading is likely to confuse contributors; update the path in the heading to the actual PowerShell script location.

Suggested change
##### PowerShell script (`scripts/update-agent-context`)
##### PowerShell script (`scripts/powershell/update-agent-context.ps1`)

Copilot uses AI. Check for mistakes.

Add file variable:

Expand Down
Loading