CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/intent-integrity-kit

Closing the intent-to-code chasm - specification-driven development with BDD verification chain

Overall
score

96%

Does it follow best practices?

Validation for skill structure

Overview
Skills
Evals
Files

create-new-feature.ps1skills/iikit-01-specify/scripts/powershell/

#!/usr/bin/env pwsh

Create a new feature

[CmdletBinding()] param( [Parameter(Position = 0, ValueFromRemainingArguments = $true)] [string[]]$FeatureDescription, [Alias('j')] [switch]$Json, [Alias('s')] [string]$ShortName, [Alias('n')] [int]$Number = 0, [Alias('b')] [switch]$SkipBranch, [Alias('h')] [switch]$Help ) $ErrorActionPreference = 'Stop'

Show help if requested

if ($Help) { Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] [-SkipBranch] <feature description>" Write-Host "" Write-Host "Options:" Write-Host " -Json Output in JSON format" Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch" Write-Host " -Number N Specify branch number manually (overrides auto-detection)" Write-Host " -SkipBranch Create feature directory without creating a git branch" Write-Host " -Help Show this help message" Write-Host "" Write-Host "Examples:" Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'" Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'" Write-Host " ./create-new-feature.ps1 -SkipBranch 'Fix bug on existing branch'" exit 0 }

Check if feature description provided

if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) { Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>" exit 1 }

$featureDesc = ($FeatureDescription -join ' ').Trim()

Resolve repository root. Prefer git information when available, but fall back

to searching for repository markers so the workflow still functions in repositories that

were initialized with --no-git.

function Find-RepositoryRoot { param( [string]$StartDir, [string[]]$Markers = @('.git', '.specify') ) $current = Resolve-Path $StartDir while ($true) { foreach ($marker in $Markers) { if (Test-Path (Join-Path $current $marker)) { return $current } } $parent = Split-Path $current -Parent if ($parent -eq $current) { # Reached filesystem root without finding markers return $null } $current = $parent } }

function Get-HighestNumberFromSpecs { param([string]$SpecsDir)

$highest = 0
if (Test-Path $SpecsDir) {
    Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
        if ($_.Name -match '^(\d+)') {
            $num = [int]$matches[1]
            if ($num -gt $highest) { $highest = $num }
        }
    }
}
return $highest

}

function Get-HighestNumberFromBranches { param()

$highest = 0
try {
    $branches = git branch -a 2>$null
    if ($LASTEXITCODE -eq 0) {
        foreach ($branch in $branches) {
            # Clean branch name: remove leading markers and remote prefixes
            $cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''

            # Extract feature number if branch matches pattern ###-*
            if ($cleanBranch -match '^(\d+)-') {
                $num = [int]$matches[1]
                if ($num -gt $highest) { $highest = $num }
            }
        }
    }
} catch {
    # If git command fails, return 0
    Write-Verbose "Could not check Git branches: $_"
}
return $highest

}

function Get-NextBranchNumber { param( [string]$SpecsDir )

# Fetch all remotes to get latest branch info (suppress errors if no remotes)
try {
    git fetch --all --prune 2>$null | Out-Null
} catch {
    # Ignore fetch errors
}

# Get highest number from ALL branches (not just matching short name)
$highestBranch = Get-HighestNumberFromBranches

# Get highest number from ALL specs (not just matching short name)
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir

# Take the maximum of both
$maxNum = [Math]::Max($highestBranch, $highestSpec)

# Return next number
return $maxNum + 1

}

function ConvertTo-CleanBranchName { param([string]$Name)

return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''

}

Check if git is available

try { $gitRoot = git rev-parse --show-toplevel 2>$null $hasGit = ($LASTEXITCODE -eq 0) } catch { $hasGit = $false $gitRoot = $null }

Determine project root: prefer current directory if it has .specify

$currentDir = Get-Location if (Test-Path (Join-Path $currentDir '.specify')) { # Current working directory is a project root (has .specify) $repoRoot = $currentDir } elseif ($hasGit -and $gitRoot) { # Fall back to git root $repoRoot = $gitRoot } else { # No git or current dir .specify, search for markers $repoRoot = Find-RepositoryRoot -StartDir $PSScriptRoot if (-not $repoRoot) { Write-Error "Error: Could not determine repository root. Please run this script from within the repository." exit 1 } }

Set-Location $repoRoot

$specsDir = Join-Path $repoRoot 'specs' New-Item -ItemType Directory -Path $specsDir -Force | Out-Null

Function to generate branch name with stop word filtering and length filtering

function Get-BranchName { param([string]$Description)

# Common stop words to filter out
$stopWords = @(
    'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
    'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
    'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must', 'shall',
    'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
    'want', 'need', 'add', 'get', 'set'
)

# Convert to lowercase and extract words (alphanumeric only)
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
$words = $cleanName -split '\s+' | Where-Object { $_ }

# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
$meaningfulWords = @()
foreach ($word in $words) {
    # Skip stop words
    if ($stopWords -contains $word) { continue }

    # Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
    if ($word.Length -ge 3) {
        $meaningfulWords += $word
    } elseif ($Description -match "\b$($word.ToUpper())\b") {
        # Keep short words if they appear as uppercase in original (likely acronyms)
        $meaningfulWords += $word
    }
}

# If we have meaningful words, use first 3-4 of them
if ($meaningfulWords.Count -gt 0) {
    $maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
    $result = ($meaningfulWords | Select-Object -First $maxWords) -join '-'
    return $result
} else {
    # Fallback to original logic if no meaningful words found
    $result = ConvertTo-CleanBranchName -Name $Description
    $fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
    return [string]::Join('-', $fallbackWords)
}

}

Generate branch name

if ($ShortName) { # Use provided short name, just clean it up $branchSuffix = ConvertTo-CleanBranchName -Name $ShortName } else { # Generate from description with smart filtering $branchSuffix = Get-BranchName -Description $featureDesc }

Determine branch number

if ($Number -eq 0) { if ($hasGit) { # Check existing branches on remotes $Number = Get-NextBranchNumber -SpecsDir $specsDir } else { # Fall back to local directory check $Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1 } }

$featureNum = ('{0:000}' -f $Number) $branchName = "$featureNum-$branchSuffix"

GitHub enforces a 244-byte limit on branch names

Validate and truncate if necessary

$maxBranchLength = 244 if ($branchName.Length -gt $maxBranchLength) { # Calculate how much we need to trim from suffix # Account for: feature number (3) + hyphen (1) = 4 chars $maxSuffixLength = $maxBranchLength - 4

# Truncate suffix
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
# Remove trailing hyphen if truncation created one
$truncatedSuffix = $truncatedSuffix -replace '-$', ''

$originalBranchName = $branchName
$branchName = "$featureNum-$truncatedSuffix"

Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"

}

if ($SkipBranch) { Write-Warning "[specify] Skipping branch creation (-SkipBranch). Feature directory: $branchName" } elseif ($hasGit) { try { git checkout -b $branchName | Out-Null } catch { Write-Warning "Failed to create git branch: $branchName" } } else { Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName" }

$featureDir = Join-Path $specsDir $branchName New-Item -ItemType Directory -Path $featureDir -Force | Out-Null

Template path relative to script location (works for both .tessl and .claude installs)

$template = Join-Path $PSScriptRoot '....\templates\spec-template.md' $specFile = Join-Path $featureDir 'spec.md' if (Test-Path $template) { Copy-Item $template $specFile -Force } else { New-Item -ItemType File -Path $specFile | Out-Null }

Set the SPECIFY_FEATURE environment variable for the current session

$env:SPECIFY_FEATURE = $branchName

Write sticky active feature (survives restarts)

. "$PSScriptRoot/common.ps1" Write-ActiveFeature -Feature $branchName -RepoRoot $repoRoot

if ($Json) { $obj = [PSCustomObject]@{ BRANCH_NAME = $branchName SPEC_FILE = $specFile FEATURE_NUM = $featureNum HAS_GIT = $hasGit } $obj | ConvertTo-Json -Compress } else { Write-Output "BRANCH_NAME: $branchName" Write-Output "SPEC_FILE: $specFile" Write-Output "FEATURE_NUM: $featureNum" Write-Output "HAS_GIT: $hasGit" }

Install with Tessl CLI

npx tessl i tessl-labs/intent-integrity-kit

skills

README.md

tile.json