Compare commits

..

14 Commits

Author SHA1 Message Date
Den Delimarsky
85e5eedef8 Merge pull request #212 from github/update-cli
Fix package logic
2025-09-12 13:31:10 -07:00
Den Delimarsky 🌺
0a5b1ac538 Fix package logic 2025-09-12 13:30:15 -07:00
Den Delimarsky
eaf4caa231 Merge pull request #211 from github/update-cli
CLI QOL improvements
2025-09-12 13:17:42 -07:00
Den Delimarsky 🌺
c29e419b4f Update config 2025-09-12 13:15:41 -07:00
Den Delimarsky 🌺
af3cf934e5 Update __init__.py 2025-09-12 10:39:51 -07:00
Den Delimarsky 🌺
5787bb5537 Refactor with platform-specific constraints 2025-09-12 10:27:43 -07:00
Den Delimarsky
d605d1e008 Merge pull request #199 from github/update-cli
Update cli
2025-09-12 00:18:11 -07:00
Den Delimarsky 🌺
57024454bf Update README.md 2025-09-12 00:17:31 -07:00
Den Delimarsky 🌺
1ae6b55c87 Update CLI reference 2025-09-12 00:15:25 -07:00
Den Delimarsky
bfeb40cebc Merge pull request #124 from STRRL/fix/compatibility-claude-migrate-installer
fix: support Claude CLI installed via migrate-installer
2025-09-12 00:01:58 -07:00
Den Delimarsky
22b7098edb Merge pull request #197 from github/update-cli
Update __init__.py
2025-09-11 23:55:17 -07:00
Den Delimarsky 🌺
38ad8b0bac Update __init__.py 2025-09-11 23:54:49 -07:00
Zhiqiang ZHOU
24ba30444e refactor: extract Claude local path to constant for maintainability
Extract the hardcoded Claude CLI local installation path to a constant
CLAUDE_LOCAL_PATH to improve maintainability and make it easier to update
if the installation path changes in the future.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-09 13:40:54 -07:00
Zhiqiang ZHOU
584175351a fix: support Claude CLI installed via migrate-installer
After running `claude migrate-installer`, the Claude executable is moved
from PATH to ~/.claude/local/claude. This change updates check_tool() to
prioritize checking this local path before falling back to PATH lookup.

Fixes: #123

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-09 13:39:00 -07:00
29 changed files with 927 additions and 793 deletions

View File

@@ -13,118 +13,115 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Get latest tag
id: get_tag
run: |
# Get the latest tag, or use v0.0.0 if no tags exist
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
- name: Get latest tag # Extract version number and increment
id: get_tag VERSION=$(echo $LATEST_TAG | sed 's/v//')
run: | IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
# Get the latest tag, or use v0.0.0 if no tags exist MAJOR=${VERSION_PARTS[0]:-0}
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") MINOR=${VERSION_PARTS[1]:-0}
echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT PATCH=${VERSION_PARTS[2]:-0}
# Extract version number and increment # Increment patch version
VERSION=$(echo $LATEST_TAG | sed 's/v//') PATCH=$((PATCH + 1))
IFS='.' read -ra VERSION_PARTS <<< "$VERSION" NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
MAJOR=${VERSION_PARTS[0]:-0}
MINOR=${VERSION_PARTS[1]:-0}
PATCH=${VERSION_PARTS[2]:-0}
# Increment patch version echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
PATCH=$((PATCH + 1)) echo "New version will be: $NEW_VERSION"
NEW_VERSION="v$MAJOR.$MINOR.$PATCH" - name: Check if release already exists
id: check_release
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT run: |
echo "New version will be: $NEW_VERSION" if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
- name: Check if release already exists echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..."
id: check_release
run: |
if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..."
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Release ${{ steps.get_tag.outputs.new_version }} does not exist, proceeding..."
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release package
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/create-release-packages.sh
.github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }}
- name: Generate release notes
if: steps.check_release.outputs.exists == 'false'
id: release_notes
run: |
# Get commits since last tag
LAST_TAG=${{ steps.get_tag.outputs.latest_tag }}
if [ "$LAST_TAG" = "v0.0.0" ]; then
# Check how many commits we have and use that as the limit
COMMIT_COUNT=$(git rev-list --count HEAD)
if [ "$COMMIT_COUNT" -gt 10 ]; then
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD)
else else
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s") echo "exists=false" >> $GITHUB_OUTPUT
echo "Release ${{ steps.get_tag.outputs.new_version }} does not exist, proceeding..."
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release package variants
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/create-release-packages.sh
.github/workflows/scripts/create-release-packages.sh ${{ steps.get_tag.outputs.new_version }}
- name: Generate release notes
if: steps.check_release.outputs.exists == 'false'
id: release_notes
run: |
# Get commits since last tag
LAST_TAG=${{ steps.get_tag.outputs.latest_tag }}
if [ "$LAST_TAG" = "v0.0.0" ]; then
# Check how many commits we have and use that as the limit
COMMIT_COUNT=$(git rev-list --count HEAD)
if [ "$COMMIT_COUNT" -gt 10 ]; then
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD)
else
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s")
fi
else
COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD)
fi fi
else
COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD)
fi
# Create release notes # Create release notes
cat > release_notes.md << EOF cat > release_notes.md << EOF
Template release ${{ steps.get_tag.outputs.new_version }} Template release ${{ steps.get_tag.outputs.new_version }}
Updated specification-driven development templates for GitHub Copilot, Claude Code, and Gemini CLI. Updated specification-driven development templates for GitHub Copilot, Claude Code, and Gemini CLI.
Download the template for your preferred AI assistant: Now includes per-script variants for POSIX shell (sh) and PowerShell (ps).
- spec-kit-template-copilot-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-claude-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-gemini-${{ steps.get_tag.outputs.new_version }}.zip
EOF
echo "Generated release notes:" Download the template for your preferred AI assistant + script type:
cat release_notes.md - spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip
EOF
- name: Create GitHub Release echo "Generated release notes:"
if: steps.check_release.outputs.exists == 'false' cat release_notes.md
run: | - name: Create GitHub Release
# Remove 'v' prefix from version for release title if: steps.check_release.outputs.exists == 'false'
VERSION_NO_V=${{ steps.get_tag.outputs.new_version }} run: |
VERSION_NO_V=${VERSION_NO_V#v} # Remove 'v' prefix from version for release title
VERSION_NO_V=${{ steps.get_tag.outputs.new_version }}
VERSION_NO_V=${VERSION_NO_V#v}
gh release create ${{ steps.get_tag.outputs.new_version }} \ gh release create ${{ steps.get_tag.outputs.new_version }} \
spec-kit-template-copilot-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-claude-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-gemini-${{ steps.get_tag.outputs.new_version }}.zip \ spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip \
--title "Spec Kit Templates - $VERSION_NO_V" \ spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip \
--notes-file release_notes.md spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip \
env: spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} --title "Spec Kit Templates - $VERSION_NO_V" \
--notes-file release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update version in pyproject.toml (for release artifacts only)
if: steps.check_release.outputs.exists == 'false'
run: |
# Update version in pyproject.toml (remove 'v' prefix for Python versioning)
VERSION=${{ steps.get_tag.outputs.new_version }}
PYTHON_VERSION=${VERSION#v}
- name: Update version in pyproject.toml (for release artifacts only) if [ -f "pyproject.toml" ]; then
if: steps.check_release.outputs.exists == 'false' sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml
run: | echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)"
# Update version in pyproject.toml (remove 'v' prefix for Python versioning) fi
VERSION=${{ steps.get_tag.outputs.new_version }}
PYTHON_VERSION=${VERSION#v}
if [ -f "pyproject.toml" ]; then
sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml
echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)"
fi
# Note: No longer committing version changes back to main branch
# The version is only updated in the release artifacts

View File

@@ -2,9 +2,16 @@
set -euo pipefail set -euo pipefail
# create-release-packages.sh (workflow-local) # create-release-packages.sh (workflow-local)
# Build Spec Kit template release archives for each supported AI assistant. # Build Spec Kit template release archives for each supported AI assistant and script type.
# Usage: .github/workflows/scripts/create-release-packages.sh <version> # Usage: .github/workflows/scripts/create-release-packages.sh <version>
# Version argument should include leading 'v'. # Version argument should include leading 'v'.
# Optionally set AGENTS and/or SCRIPTS env vars to limit what gets built.
# AGENTS : space or comma separated subset of: claude gemini copilot (default: all)
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
# Examples:
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
# AGENTS="copilot,gemini" $0 v0.2.0
# SCRIPTS=ps $0 v0.2.0
if [[ $# -ne 1 ]]; then if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version-with-v-prefix>" >&2 echo "Usage: $0 <version-with-v-prefix>" >&2
@@ -18,10 +25,7 @@ fi
echo "Building release packages for $NEW_VERSION" echo "Building release packages for $NEW_VERSION"
rm -rf sdd-package-base sdd-claude-package sdd-gemini-package sdd-copilot-package \ rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-${NEW_VERSION}.zip || true
spec-kit-template-claude-${NEW_VERSION}.zip \
spec-kit-template-gemini-${NEW_VERSION}.zip \
spec-kit-template-copilot-${NEW_VERSION}.zip || true
mkdir -p sdd-package-base mkdir -p sdd-package-base
SPEC_DIR="sdd-package-base/.specify" SPEC_DIR="sdd-package-base/.specify"
@@ -29,7 +33,7 @@ mkdir -p "$SPEC_DIR"
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; } [[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
[[ -d scripts ]] && { cp -r scripts "$SPEC_DIR/"; echo "Copied scripts -> .specify/scripts"; } [[ -d scripts ]] && { cp -r scripts "$SPEC_DIR/"; echo "Copied scripts -> .specify/scripts"; }
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \;; echo "Copied templates -> .specify/templates"; } [[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
rewrite_paths() { rewrite_paths() {
sed -E \ sed -E \
@@ -39,54 +43,136 @@ rewrite_paths() {
} }
generate_commands() { generate_commands() {
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5
mkdir -p "$output_dir" mkdir -p "$output_dir"
for template in templates/commands/*.md; do for template in templates/commands/*.md; do
[[ -f "$template" ]] || continue [[ -f "$template" ]] || continue
local name description body local name description raw_body variant_line injected body file_norm delim_count
name=$(basename "$template" .md) name=$(basename "$template" .md)
description=$(awk '/^description:/ {gsub(/^description: *"?/, ""); gsub(/"$/, ""); print; exit}' "$template" | tr -d '\r') # Normalize line endings first (remove CR) for consistent regex matching
body=$(awk '/^---$/{if(++count==2) start=1; next} start' "$template" | sed "s/{ARGS}/$arg_format/g" | rewrite_paths) file_norm=$(tr -d '\r' < "$template")
# Extract description from frontmatter
description=$(printf '%s\n' "$file_norm" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
# Count YAML frontmatter delimiter lines
delim_count=$(printf '%s\n' "$file_norm" | grep -c '^---$' || true)
if [[ $delim_count -ge 2 ]]; then
# Grab everything after the second --- line
raw_body=$(printf '%s\n' "$file_norm" | awk '/^---$/ {if(++c==2){next}; if(c>=2){print}}')
else
# Fallback: no proper frontmatter detected; use entire file content (still allowing variant parsing)
raw_body=$file_norm
fi
# If somehow still empty, fallback once more to whole normalized file
if [[ -z ${raw_body// /} ]]; then
echo "Warning: body extraction empty for $template; using full file" >&2
raw_body=$file_norm
fi
# Find single-line variant comment matching the variant: <!-- VARIANT:sh ... --> or <!-- VARIANT:ps ... -->
variant_line=$(printf '%s\n' "$raw_body" | awk -v sv="$script_variant" '/<!--[[:space:]]+VARIANT:'sv'/ {match($0, /VARIANT:'"sv"'[[:space:]]+(.*)-->/, m); if (m[1]!="") {print m[1]; exit}}')
if [[ -z $variant_line ]]; then
echo "Warning: no variant line found for $script_variant in $template" >&2
variant_line="(Missing variant command for $script_variant)"
fi
# Replace the token VARIANT-INJECT with the selected variant line
injected=$(printf '%s\n' "$raw_body" | sed "s/VARIANT-INJECT/${variant_line//\//\/}/")
# Remove all single-line variant comments
injected=$(printf '%s\n' "$injected" | sed '/<!--[[:space:]]*VARIANT:sh/d' | sed '/<!--[[:space:]]*VARIANT:ps/d')
# Guard: if after stripping variant lines and injection the body became empty, restore original (minus variant comments) to avoid empty prompt files
if [[ -z ${injected// /} ]]; then
echo "Warning: resulting injected body empty for $template; writing unmodified body" >&2
injected=$raw_body
fi
# Apply arg substitution and path rewrite
body=$(printf '%s\n' "$injected" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
case $ext in case $ext in
toml) toml)
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;; { echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;;
md) md)
echo "$body" > "$output_dir/$name.$ext" ;; echo "$body" > "$output_dir/$name.$ext" ;;
prompt.md) prompt.md)
sed "s/{ARGS}/$arg_format/g" "$template" | rewrite_paths > "$output_dir/$name.$ext" ;; echo "$body" > "$output_dir/$name.$ext" ;;
esac esac
done done
} }
# Create Claude package build_variant() {
echo "Building Claude package..." local agent=$1 script=$2
mkdir -p sdd-claude-package local base_dir="sdd-${agent}-package-${script}"
cp -r sdd-package-base/. sdd-claude-package/ echo "Building $agent ($script) package..."
mkdir -p sdd-claude-package/.claude/commands mkdir -p "$base_dir"
generate_commands claude md "\$ARGUMENTS" sdd-claude-package/.claude/commands cp -r sdd-package-base/. "$base_dir"/
echo "Created Claude package" # Inject variant into plan-template.md within .specify/templates if present
local plan_tpl="$base_dir/.specify/templates/plan-template.md"
if [[ -f "$plan_tpl" ]]; then
variant_line=$(awk -v sv="$script" '/<!--[[:space:]]*VARIANT:'"$script"'/ {match($0, /VARIANT:'"$script"'[[:space:]]+(.*)-->/, m); if(m[1]!=""){print m[1]; exit}}' "$plan_tpl")
if [[ -n $variant_line ]]; then
tmp_file=$(mktemp)
sed "s/VARIANT-INJECT/${variant_line//\//\/}/" "$plan_tpl" | sed "/__AGENT__/s//${agent}/g" | sed '/<!--[[:space:]]*VARIANT:sh/d' | sed '/<!--[[:space:]]*VARIANT:ps/d' > "$tmp_file" && mv "$tmp_file" "$plan_tpl"
else
echo "Warning: no plan-template variant for $script" >&2
fi
fi
case $agent in
claude)
mkdir -p "$base_dir/.claude/commands"
generate_commands claude md "\$ARGUMENTS" "$base_dir/.claude/commands" "$script" ;;
gemini)
mkdir -p "$base_dir/.gemini/commands"
generate_commands gemini toml "{{args}}" "$base_dir/.gemini/commands" "$script"
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md "$base_dir/GEMINI.md" ;;
copilot)
mkdir -p "$base_dir/.github/prompts"
generate_commands copilot prompt.md "\$ARGUMENTS" "$base_dir/.github/prompts" "$script" ;;
esac
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
echo "Created spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
}
# Create Gemini package # Determine agent list
echo "Building Gemini package..." ALL_AGENTS=(claude gemini copilot)
mkdir -p sdd-gemini-package ALL_SCRIPTS=(sh ps)
cp -r sdd-package-base/. sdd-gemini-package/
mkdir -p sdd-gemini-package/.gemini/commands
generate_commands gemini toml "{{args}}" sdd-gemini-package/.gemini/commands
[[ -f agent_templates/gemini/GEMINI.md ]] && cp agent_templates/gemini/GEMINI.md sdd-gemini-package/GEMINI.md
echo "Created Gemini package"
# Create Copilot package norm_list() {
echo "Building Copilot package..." # convert comma+space separated -> space separated unique while preserving order of first occurrence
mkdir -p sdd-copilot-package tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i)}}}END{printf("\n")}'
cp -r sdd-package-base/. sdd-copilot-package/ }
mkdir -p sdd-copilot-package/.github/prompts
generate_commands copilot prompt.md "\$ARGUMENTS" sdd-copilot-package/.github/prompts
echo "Created Copilot package"
( cd sdd-claude-package && zip -r ../spec-kit-template-claude-${NEW_VERSION}.zip . ) validate_subset() {
( cd sdd-gemini-package && zip -r ../spec-kit-template-gemini-${NEW_VERSION}.zip . ) local type=$1; shift; local -n allowed=$1; shift; local items=($@)
( cd sdd-copilot-package && zip -r ../spec-kit-template-copilot-${NEW_VERSION}.zip . ) local ok=1
for it in "${items[@]}"; do
local found=0
for a in "${allowed[@]}"; do [[ $it == $a ]] && { found=1; break; }; done
if [[ $found -eq 0 ]]; then
echo "Error: unknown $type '$it' (allowed: ${allowed[*]})" >&2
ok=0
fi
done
return $ok
}
if [[ -n ${AGENTS:-} ]]; then
AGENT_LIST=($(printf '%s' "$AGENTS" | norm_list))
validate_subset agent ALL_AGENTS "${AGENT_LIST[@]}" || exit 1
else
AGENT_LIST=(${ALL_AGENTS[@]})
fi
if [[ -n ${SCRIPTS:-} ]]; then
SCRIPT_LIST=($(printf '%s' "$SCRIPTS" | norm_list))
validate_subset script ALL_SCRIPTS "${SCRIPT_LIST[@]}" || exit 1
else
SCRIPT_LIST=(${ALL_SCRIPTS[@]})
fi
echo "Agents: ${AGENT_LIST[*]}"
echo "Scripts: ${SCRIPT_LIST[*]}"
for agent in "${AGENT_LIST[@]}"; do
for script in "${SCRIPT_LIST[@]}"; do
build_variant "$agent" "$script"
done
done
echo "Archives:" echo "Archives:"
ls -1 spec-kit-template-*-${NEW_VERSION}.zip ls -1 spec-kit-template-*-${NEW_VERSION}.zip
unzip -l spec-kit-template-copilot-${NEW_VERSION}.zip | head -10 || true

View File

@@ -16,6 +16,7 @@
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development) - [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
- [⚡ Get started](#-get-started) - [⚡ Get started](#-get-started)
- [🔧 Specify CLI Reference](#-specify-cli-reference)
- [📚 Core philosophy](#-core-philosophy) - [📚 Core philosophy](#-core-philosophy)
- [🌟 Development phases](#-development-phases) - [🌟 Development phases](#-development-phases)
- [🎯 Experimental goals](#-experimental-goals) - [🎯 Experimental goals](#-experimental-goals)
@@ -64,6 +65,47 @@ Use `/tasks` to create an actionable task list, then ask your agent to implement
For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md). For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md).
## 🔧 Specify CLI Reference
The `specify` command supports the following options:
### Commands
| Command | Description |
|-------------|----------------------------------------------------------------|
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`) |
### `specify init` Arguments & Options
| Argument/Option | Type | Description |
|------------------------|----------|------------------------------------------------------------------------------|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, or `copilot` |
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
| `--no-git` | Flag | Skip git repository initialization |
| `--here` | Flag | Initialize project in the current directory instead of creating a new one |
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
### Examples
```bash
# Basic project initialization
specify init my-project
# Initialize with specific AI assistant
specify init my-project --ai claude
# Initialize in current directory
specify init --here --ai copilot
# Skip git initialization
specify init my-project --ai gemini --no-git
# Check system requirements
specify check
```
## 📚 Core philosophy ## 📚 Core philosophy
Spec-Driven Development is a structured process that emphasizes: Spec-Driven Development is a structured process that emphasizes:
@@ -214,7 +256,6 @@ At this stage, your project folder contents should resemble the following:
│ └── 001-create-taskify │ └── 001-create-taskify
│ └── spec.md │ └── spec.md
└── templates └── templates
├── CLAUDE-template.md
├── plan-template.md ├── plan-template.md
├── spec-template.md ├── spec-template.md
└── tasks-template.md └── tasks-template.md

View File

@@ -2,7 +2,7 @@
## Prerequisites ## Prerequisites
- **Linux/macOS** (or WSL2 on Windows) - **Linux/macOS** (or Windows; PowerShell scripts now supported without WSL)
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) - AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli)
- [uv](https://docs.astral.sh/uv/) for package management - [uv](https://docs.astral.sh/uv/) for package management
- [Python 3.11+](https://www.python.org/downloads/) - [Python 3.11+](https://www.python.org/downloads/)
@@ -34,6 +34,21 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <project_name
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai copilot uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai copilot
``` ```
### Specify Script Type (Shell vs PowerShell)
All automation scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants.
Auto behavior:
- Windows default: `ps`
- Other OS default: `sh`
- Interactive mode: you'll be prompted unless you pass `--script`
Force a specific script type:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script sh
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script ps
```
### Ignore Agent Tools Check ### Ignore Agent Tools Check
If you prefer to get the templates without checking for the right tools: If you prefer to get the templates without checking for the right tools:
@@ -49,6 +64,8 @@ After initialization, you should see the following commands available in your AI
- `/plan` - Generate implementation plans - `/plan` - Generate implementation plans
- `/tasks` - Break down into actionable tasks - `/tasks` - Break down into actionable tasks
The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.
## Troubleshooting ## Troubleshooting
### Git Credential Manager on Linux ### Git Credential Manager on Linux

View File

@@ -2,6 +2,8 @@
This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first. This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first.
> Scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants. The CLI auto-selects based on OS unless you pass `--script sh|ps`.
## 1. Clone and Switch Branches ## 1. Clone and Switch Branches
```bash ```bash
@@ -18,13 +20,13 @@ You can execute the CLI via the module entrypoint without installing anything:
```bash ```bash
# From repo root # From repo root
python -m src.specify_cli --help python -m src.specify_cli --help
python -m src.specify_cli init demo-project --ai claude --ignore-agent-tools python -m src.specify_cli init demo-project --ai claude --ignore-agent-tools --script sh
``` ```
If you prefer invoking the script file style (uses shebang): If you prefer invoking the script file style (uses shebang):
```bash ```bash
python src/specify_cli/__init__.py init demo-project python src/specify_cli/__init__.py init demo-project --script ps
``` ```
## 3. Use Editable Install (Isolated Environment) ## 3. Use Editable Install (Isolated Environment)
@@ -34,7 +36,7 @@ Create an isolated environment using `uv` so dependencies resolve exactly like e
```bash ```bash
# Create & activate virtual env (uv auto-manages .venv) # Create & activate virtual env (uv auto-manages .venv)
uv venv uv venv
source .venv/bin/activate # or on Windows: .venv\\Scripts\\activate source .venv/bin/activate # or on Windows PowerShell: .venv\Scripts\Activate.ps1
# Install project in editable mode # Install project in editable mode
uv pip install -e . uv pip install -e .
@@ -50,7 +52,7 @@ Re-running after code edits requires no reinstall because of editable mode.
`uvx` can run from a local path (or a Git ref) to simulate user flows: `uvx` can run from a local path (or a Git ref) to simulate user flows:
```bash ```bash
uvx --from . specify init demo-uvx --ai copilot --ignore-agent-tools uvx --from . specify init demo-uvx --ai copilot --ignore-agent-tools --script sh
``` ```
You can also point uvx at a specific branch without merging: You can also point uvx at a specific branch without merging:
@@ -58,7 +60,7 @@ You can also point uvx at a specific branch without merging:
```bash ```bash
# Push your working branch first # Push your working branch first
git push origin your-feature-branch git push origin your-feature-branch
uvx --from git+https://github.com/github/spec-kit.git@your-feature-branch specify init demo-branch-test uvx --from git+https://github.com/github/spec-kit.git@your-feature-branch specify init demo-branch-test --script ps
``` ```
### 4a. Absolute Path uvx (Run From Anywhere) ### 4a. Absolute Path uvx (Run From Anywhere)
@@ -67,13 +69,13 @@ If you're in another directory, use an absolute path instead of `.`:
```bash ```bash
uvx --from /mnt/c/GitHub/spec-kit specify --help uvx --from /mnt/c/GitHub/spec-kit specify --help
uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --ai copilot --ignore-agent-tools uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --ai copilot --ignore-agent-tools --script sh
``` ```
Set an environment variable for convenience: Set an environment variable for convenience:
```bash ```bash
export SPEC_KIT_SRC=/mnt/c/GitHub/spec-kit export SPEC_KIT_SRC=/mnt/c/GitHub/spec-kit
uvx --from "$SPEC_KIT_SRC" specify init demo-env --ai copilot --ignore-agent-tools uvx --from "$SPEC_KIT_SRC" specify init demo-env --ai copilot --ignore-agent-tools --script ps
``` ```
(Optional) Define a shell function: (Optional) Define a shell function:
@@ -91,7 +93,7 @@ After running an `init`, check that shell scripts are executable on POSIX system
ls -l scripts | grep .sh ls -l scripts | grep .sh
# Expect owner execute bit (e.g. -rwxr-xr-x) # Expect owner execute bit (e.g. -rwxr-xr-x)
``` ```
On Windows this step is a no-op. On Windows you will instead use the `.ps1` scripts (no chmod needed).
## 6. Run Lint / Basic Checks (Add Your Own) ## 6. Run Lint / Basic Checks (Add Your Own)
@@ -116,7 +118,7 @@ When testing `init --here` in a dirty directory, create a temp workspace:
```bash ```bash
mkdir /tmp/spec-test && cd /tmp/spec-test mkdir /tmp/spec-test && cd /tmp/spec-test
python -m src.specify_cli init --here --ai claude --ignore-agent-tools # if repo copied here python -m src.specify_cli init --here --ai claude --ignore-agent-tools --script sh # if repo copied here
``` ```
Or copy only the modified CLI portion if you want a lighter sandbox. Or copy only the modified CLI portion if you want a lighter sandbox.
@@ -126,7 +128,7 @@ If you need to bypass TLS validation while experimenting:
```bash ```bash
specify check --skip-tls specify check --skip-tls
specify init demo --skip-tls --ai gemini --ignore-agent-tools specify init demo --skip-tls --ai gemini --ignore-agent-tools --script ps
``` ```
(Use only for local experimentation.) (Use only for local experimentation.)
@@ -153,8 +155,9 @@ rm -rf .venv dist build *.egg-info
| Symptom | Fix | | Symptom | Fix |
|---------|-----| |---------|-----|
| `ModuleNotFoundError: typer` | Run `uv pip install -e .` | | `ModuleNotFoundError: typer` | Run `uv pip install -e .` |
| Scripts not executable (Linux) | Re-run init (logic adds bits) or `chmod +x scripts/*.sh` | | Scripts not executable (Linux) | Re-run init or `chmod +x scripts/*.sh` |
| Git step skipped | You passed `--no-git` or Git not installed | | Git step skipped | You passed `--no-git` or Git not installed |
| Wrong script type downloaded | Pass `--script sh` or `--script ps` explicitly |
| TLS errors on corporate network | Try `--skip-tls` (not for production) | | TLS errors on corporate network | Try `--skip-tls` (not for production) |
## 13. Next Steps ## 13. Next Steps

View File

@@ -2,6 +2,8 @@
This guide will help you get started with Spec-Driven Development using Spec Kit. This guide will help you get started with Spec-Driven Development using Spec Kit.
> NEW: All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
## The 4-Step Process ## The 4-Step Process
### 1. Install Specify ### 1. Install Specify
@@ -12,6 +14,12 @@ Initialize your project depending on the coding agent you're using:
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
``` ```
Pick script type explicitly (optional):
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script ps # Force PowerShell
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell
```
### 2. Create the Spec ### 2. Create the Spec
Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack. Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e
JSON_MODE=false
for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
eval $(get_feature_paths)
check_feature_branch "$CURRENT_BRANCH" || exit 1
if [[ ! -d "$FEATURE_DIR" ]]; then echo "ERROR: Feature directory not found: $FEATURE_DIR"; echo "Run /specify first."; exit 1; fi
if [[ ! -f "$IMPL_PLAN" ]]; then echo "ERROR: plan.md not found in $FEATURE_DIR"; echo "Run /plan first."; exit 1; fi
if $JSON_MODE; then
docs=(); [[ -f "$RESEARCH" ]] && docs+=("research.md"); [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md"); ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/"); [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md");
json_docs=$(printf '"%s",' "${docs[@]}"); json_docs="[${json_docs%,}]"; printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
else
echo "FEATURE_DIR:$FEATURE_DIR"; echo "AVAILABLE_DOCS:"; check_file "$RESEARCH" "research.md"; check_file "$DATA_MODEL" "data-model.md"; check_dir "$CONTRACTS_DIR" "contracts/"; check_file "$QUICKSTART" "quickstart.md"; fi

37
scripts/bash/common.sh Normal file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# (Moved to scripts/bash/) Common functions and variables for all scripts
get_repo_root() { git rev-parse --show-toplevel; }
get_current_branch() { git rev-parse --abbrev-ref HEAD; }
check_feature_branch() {
local branch="$1"
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
echo "Feature branches should be named like: 001-feature-name" >&2
return 1
fi; return 0
}
get_feature_dir() { echo "$1/specs/$2"; }
get_feature_paths() {
local repo_root=$(get_repo_root)
local current_branch=$(get_current_branch)
local feature_dir=$(get_feature_dir "$repo_root" "$current_branch")
cat <<EOF
REPO_ROOT='$repo_root'
CURRENT_BRANCH='$current_branch'
FEATURE_DIR='$feature_dir'
FEATURE_SPEC='$feature_dir/spec.md'
IMPL_PLAN='$feature_dir/plan.md'
TASKS='$feature_dir/tasks.md'
RESEARCH='$feature_dir/research.md'
DATA_MODEL='$feature_dir/data-model.md'
QUICKSTART='$feature_dir/quickstart.md'
CONTRACTS_DIR='$feature_dir/contracts'
EOF
}
check_file() { [[ -f "$1" ]] && echo "$2" || echo "$2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo "$2" || echo "$2"; }

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# (Moved to scripts/bash/) Create a new feature with branch, directory structure, and template
set -e
JSON_MODE=false
ARGS=()
for arg in "$@"; do
case "$arg" in
--json) JSON_MODE=true ;;
--help|-h) echo "Usage: $0 [--json] <feature_description>"; exit 0 ;;
*) ARGS+=("$arg") ;;
esac
done
FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] <feature_description>" >&2
exit 1
fi
REPO_ROOT=$(git rev-parse --show-toplevel)
SPECS_DIR="$REPO_ROOT/specs"
mkdir -p "$SPECS_DIR"
HIGHEST=0
if [ -d "$SPECS_DIR" ]; then
for dir in "$SPECS_DIR"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
done
fi
NEXT=$((HIGHEST + 1))
FEATURE_NUM=$(printf "%03d" "$NEXT")
BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//')
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
git checkout -b "$BRANCH_NAME"
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"
TEMPLATE="$REPO_ROOT/templates/spec-template.md"
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
if $JSON_MODE; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
fi

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
eval $(get_feature_paths)
check_feature_branch "$CURRENT_BRANCH" || exit 1
echo "REPO_ROOT: $REPO_ROOT"; echo "BRANCH: $CURRENT_BRANCH"; echo "FEATURE_DIR: $FEATURE_DIR"; echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "TASKS: $TASKS"

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -e
JSON_MODE=false
for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
eval $(get_feature_paths)
check_feature_branch "$CURRENT_BRANCH" || exit 1
mkdir -p "$FEATURE_DIR"
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
[[ -f "$TEMPLATE" ]] && cp "$TEMPLATE" "$IMPL_PLAN"
if $JSON_MODE; then
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH"
else
echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "SPECS_DIR: $FEATURE_DIR"; echo "BRANCH: $CURRENT_BRANCH"
fi

View File

@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -e
REPO_ROOT=$(git rev-parse --show-toplevel)
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
NEW_PLAN="$FEATURE_DIR/plan.md"
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"; GEMINI_FILE="$REPO_ROOT/GEMINI.md"; COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
AGENT_TYPE="$1"
[ -f "$NEW_PLAN" ] || { echo "ERROR: No plan.md found at $NEW_PLAN"; exit 1; }
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "")
update_agent_file() { local target_file="$1" agent_name="$2"; echo "Updating $agent_name context file: $target_file"; local temp_file=$(mktemp); if [ ! -f "$target_file" ]; then
echo "Creating new $agent_name context file..."; if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file"; else echo "ERROR: Template not found"; return 1; fi;
sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file"; sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file"; sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file";
if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file"; else sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file"; fi;
if [[ "$NEW_LANG" == *"Python"* ]]; then COMMANDS="cd src && pytest && ruff check ."; elif [[ "$NEW_LANG" == *"Rust"* ]]; then COMMANDS="cargo test && cargo clippy"; elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then COMMANDS="npm test && npm run lint"; else COMMANDS="# Add commands for $NEW_LANG"; fi; sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file";
sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"; sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file"; rm "$temp_file.bak";
else
echo "Updating existing $agent_name context file..."; manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1); manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$target_file" | cut -d: -f1); if [ -n "$manual_start" ] && [ -n "$manual_end" ]; then sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt; fi;
python3 - "$target_file" <<'EOF'
import re,sys,datetime
target=sys.argv[1]
with open(target) as f: content=f.read()
NEW_LANG="'$NEW_LANG'";NEW_FRAMEWORK="'$NEW_FRAMEWORK'";CURRENT_BRANCH="'$CURRENT_BRANCH'";NEW_DB="'$NEW_DB'";NEW_PROJECT_TYPE="'$NEW_PROJECT_TYPE'"
# Tech section
m=re.search(r'## Active Technologies\n(.*?)\n\n',content, re.DOTALL)
if m:
existing=m.group(1)
additions=[]
if '$NEW_LANG' and '$NEW_LANG' not in existing: additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)")
if '$NEW_DB' and '$NEW_DB' not in existing and '$NEW_DB'!='N/A': additions.append(f"- $NEW_DB ($CURRENT_BRANCH)")
if additions:
new_block=existing+"\n"+"\n".join(additions)
content=content.replace(m.group(0),f"## Active Technologies\n{new_block}\n\n")
# Recent changes
m2=re.search(r'## Recent Changes\n(.*?)(\n\n|$)',content, re.DOTALL)
if m2:
lines=[l for l in m2.group(1).strip().split('\n') if l]
lines.insert(0,f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK")
lines=lines[:3]
content=re.sub(r'## Recent Changes\n.*?(\n\n|$)', '## Recent Changes\n'+"\n".join(lines)+'\n\n', content, flags=re.DOTALL)
content=re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', 'Last updated: '+datetime.datetime.now().strftime('%Y-%m-%d'), content)
open(target+'.tmp','w').write(content)
EOF
mv "$target_file.tmp" "$target_file"; if [ -f /tmp/manual_additions.txt ]; then sed -i.bak '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/d' "$target_file"; cat /tmp/manual_additions.txt >> "$target_file"; rm /tmp/manual_additions.txt "$target_file.bak"; fi;
fi; mv "$temp_file" "$target_file" 2>/dev/null || true; echo "$agent_name context file updated successfully"; }
case "$AGENT_TYPE" in
claude) update_agent_file "$CLAUDE_FILE" "Claude Code" ;;
gemini) update_agent_file "$GEMINI_FILE" "Gemini CLI" ;;
copilot) update_agent_file "$COPILOT_FILE" "GitHub Copilot" ;;
"") [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"; [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"; [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"; if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;;
*) echo "ERROR: Unknown agent type '$AGENT_TYPE'"; exit 1 ;;
esac
echo; echo "Summary of changes:"; [ -n "$NEW_LANG" ] && echo "- Added language: $NEW_LANG"; [ -n "$NEW_FRAMEWORK" ] && echo "- Added framework: $NEW_FRAMEWORK"; [ -n "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ] && echo "- Added database: $NEW_DB"; echo; echo "Usage: $0 [claude|gemini|copilot]"

View File

@@ -1,62 +0,0 @@
#!/usr/bin/env bash
# Check that implementation plan exists and find optional design documents
# Usage: ./check-task-prerequisites.sh [--json]
set -e
JSON_MODE=false
for arg in "$@"; do
case "$arg" in
--json) JSON_MODE=true ;;
--help|-h) echo "Usage: $0 [--json]"; exit 0 ;;
esac
done
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths
eval $(get_feature_paths)
# Check if on feature branch
check_feature_branch "$CURRENT_BRANCH" || exit 1
# Check if feature directory exists
if [[ ! -d "$FEATURE_DIR" ]]; then
echo "ERROR: Feature directory not found: $FEATURE_DIR"
echo "Run /specify first to create the feature structure."
exit 1
fi
# Check for implementation plan (required)
if [[ ! -f "$IMPL_PLAN" ]]; then
echo "ERROR: plan.md not found in $FEATURE_DIR"
echo "Run /plan first to create the plan."
exit 1
fi
if $JSON_MODE; then
# Build JSON array of available docs that actually exist
docs=()
[[ -f "$RESEARCH" ]] && docs+=("research.md")
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/")
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
# join array into JSON
json_docs=$(printf '"%s",' "${docs[@]}")
json_docs="[${json_docs%,}]"
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
else
# List available design documents (optional)
echo "FEATURE_DIR:$FEATURE_DIR"
echo "AVAILABLE_DOCS:"
# Use common check functions
check_file "$RESEARCH" "research.md"
check_file "$DATA_MODEL" "data-model.md"
check_dir "$CONTRACTS_DIR" "contracts/"
check_file "$QUICKSTART" "quickstart.md"
fi
# Always succeed - task generation should work with whatever docs are available

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env bash
# Common functions and variables for all scripts
# Get repository root
get_repo_root() {
git rev-parse --show-toplevel
}
# Get current branch
get_current_branch() {
git rev-parse --abbrev-ref HEAD
}
# Check if current branch is a feature branch
# Returns 0 if valid, 1 if not
check_feature_branch() {
local branch="$1"
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
echo "ERROR: Not on a feature branch. Current branch: $branch"
echo "Feature branches should be named like: 001-feature-name"
return 1
fi
return 0
}
# Get feature directory path
get_feature_dir() {
local repo_root="$1"
local branch="$2"
echo "$repo_root/specs/$branch"
}
# Get all standard paths for a feature
# Usage: eval $(get_feature_paths)
# Sets: REPO_ROOT, CURRENT_BRANCH, FEATURE_DIR, FEATURE_SPEC, IMPL_PLAN, TASKS
get_feature_paths() {
local repo_root=$(get_repo_root)
local current_branch=$(get_current_branch)
local feature_dir=$(get_feature_dir "$repo_root" "$current_branch")
echo "REPO_ROOT='$repo_root'"
echo "CURRENT_BRANCH='$current_branch'"
echo "FEATURE_DIR='$feature_dir'"
echo "FEATURE_SPEC='$feature_dir/spec.md'"
echo "IMPL_PLAN='$feature_dir/plan.md'"
echo "TASKS='$feature_dir/tasks.md'"
echo "RESEARCH='$feature_dir/research.md'"
echo "DATA_MODEL='$feature_dir/data-model.md'"
echo "QUICKSTART='$feature_dir/quickstart.md'"
echo "CONTRACTS_DIR='$feature_dir/contracts'"
}
# Check if a file exists and report
check_file() {
local file="$1"
local description="$2"
if [[ -f "$file" ]]; then
echo "$description"
return 0
else
echo "$description"
return 1
fi
}
# Check if a directory exists and has files
check_dir() {
local dir="$1"
local description="$2"
if [[ -d "$dir" ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]; then
echo "$description"
return 0
else
echo "$description"
return 1
fi
}

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env bash
# Create a new feature with branch, directory structure, and template
# Usage: ./create-new-feature.sh "feature description"
# ./create-new-feature.sh --json "feature description"
set -e
JSON_MODE=false
# Collect non-flag args
ARGS=()
for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--help|-h)
echo "Usage: $0 [--json] <feature_description>"; exit 0 ;;
*)
ARGS+=("$arg") ;;
esac
done
FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] <feature_description>" >&2
exit 1
fi
# Get repository root
REPO_ROOT=$(git rev-parse --show-toplevel)
SPECS_DIR="$REPO_ROOT/specs"
# Create specs directory if it doesn't exist
mkdir -p "$SPECS_DIR"
# Find the highest numbered feature directory
HIGHEST=0
if [ -d "$SPECS_DIR" ]; then
for dir in "$SPECS_DIR"/*; do
if [ -d "$dir" ]; then
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST" ]; then
HIGHEST=$number
fi
fi
done
fi
# Generate next feature number with zero padding
NEXT=$((HIGHEST + 1))
FEATURE_NUM=$(printf "%03d" "$NEXT")
# Create branch name from description
BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | \
tr '[:upper:]' '[:lower:]' | \
sed 's/[^a-z0-9]/-/g' | \
sed 's/-\+/-/g' | \
sed 's/^-//' | \
sed 's/-$//')
# Extract 2-3 meaningful words
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
# Final branch name
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
# Create and switch to new branch
git checkout -b "$BRANCH_NAME"
# Create feature directory
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"
# Copy template if it exists
TEMPLATE="$REPO_ROOT/templates/spec-template.md"
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -f "$TEMPLATE" ]; then
cp "$TEMPLATE" "$SPEC_FILE"
else
echo "Warning: Template not found at $TEMPLATE" >&2
touch "$SPEC_FILE"
fi
if $JSON_MODE; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' \
"$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
else
# Output results for the LLM to use (legacy key: value format)
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
fi

View File

@@ -1,23 +0,0 @@
#!/usr/bin/env bash
# Get paths for current feature branch without creating anything
# Used by commands that need to find existing feature files
set -e
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths
eval $(get_feature_paths)
# Check if on feature branch
check_feature_branch "$CURRENT_BRANCH" || exit 1
# Output paths (don't create anything)
echo "REPO_ROOT: $REPO_ROOT"
echo "BRANCH: $CURRENT_BRANCH"
echo "FEATURE_DIR: $FEATURE_DIR"
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "TASKS: $TASKS"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param([switch]$Json)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/common.ps1"
$paths = Get-FeaturePathsEnv
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
Write-Output "Run /specify first to create the feature structure."
exit 1
}
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
Write-Output "Run /plan first to create the plan."
exit 1
}
if ($Json) {
$docs = @()
if (Test-Path $paths.RESEARCH) { $docs += 'research.md' }
if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' }
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) { $docs += 'contracts/' }
if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' }
[PSCustomObject]@{ FEATURE_DIR=$paths.FEATURE_DIR; AVAILABLE_DOCS=$docs } | ConvertTo-Json -Compress
} else {
Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)"
Write-Output "AVAILABLE_DOCS:"
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
}

View File

@@ -0,0 +1,65 @@
#!/usr/bin/env pwsh
# Common PowerShell functions analogous to common.sh (moved to powershell/)
function Get-RepoRoot {
git rev-parse --show-toplevel
}
function Get-CurrentBranch {
git rev-parse --abbrev-ref HEAD
}
function Test-FeatureBranch {
param([string]$Branch)
if ($Branch -notmatch '^[0-9]{3}-') {
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
Write-Output "Feature branches should be named like: 001-feature-name"
return $false
}
return $true
}
function Get-FeatureDir {
param([string]$RepoRoot, [string]$Branch)
Join-Path $RepoRoot "specs/$Branch"
}
function Get-FeaturePathsEnv {
$repoRoot = Get-RepoRoot
$currentBranch = Get-CurrentBranch
$featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch
[PSCustomObject]@{
REPO_ROOT = $repoRoot
CURRENT_BRANCH = $currentBranch
FEATURE_DIR = $featureDir
FEATURE_SPEC = Join-Path $featureDir 'spec.md'
IMPL_PLAN = Join-Path $featureDir 'plan.md'
TASKS = Join-Path $featureDir 'tasks.md'
RESEARCH = Join-Path $featureDir 'research.md'
DATA_MODEL = Join-Path $featureDir 'data-model.md'
QUICKSTART = Join-Path $featureDir 'quickstart.md'
CONTRACTS_DIR = Join-Path $featureDir 'contracts'
}
}
function Test-FileExists {
param([string]$Path, [string]$Description)
if (Test-Path -Path $Path -PathType Leaf) {
Write-Output "$Description"
return $true
} else {
Write-Output "$Description"
return $false
}
}
function Test-DirHasFiles {
param([string]$Path, [string]$Description)
if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) {
Write-Output "$Description"
return $true
} else {
Write-Output "$Description"
return $false
}
}

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env pwsh
# Create a new feature (moved to powershell/)
[CmdletBinding()]
param(
[switch]$Json,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$FeatureDescription
)
$ErrorActionPreference = 'Stop'
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
Write-Error "Usage: ./create-new-feature.ps1 [-Json] <feature description>"; exit 1
}
$featureDesc = ($FeatureDescription -join ' ').Trim()
$repoRoot = git rev-parse --show-toplevel
$specsDir = Join-Path $repoRoot 'specs'
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
$highest = 0
if (Test-Path $specsDir) {
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
$next = $highest + 1
$featureNum = ('{0:000}' -f $next)
$branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
$words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3
$branchName = "$featureNum-$([string]::Join('-', $words))"
git checkout -b $branchName | Out-Null
$featureDir = Join-Path $specsDir $branchName
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
$template = Join-Path $repoRoot '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 }
if ($Json) {
$obj = [PSCustomObject]@{ BRANCH_NAME = $branchName; SPEC_FILE = $specFile; FEATURE_NUM = $featureNum }
$obj | ConvertTo-Json -Compress
} else {
Write-Output "BRANCH_NAME: $branchName"
Write-Output "SPEC_FILE: $specFile"
Write-Output "FEATURE_NUM: $featureNum"
}

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env pwsh
param()
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/common.ps1"
$paths = Get-FeaturePathsEnv
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
Write-Output "REPO_ROOT: $($paths.REPO_ROOT)"
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
Write-Output "TASKS: $($paths.TASKS)"

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param([switch]$Json)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/common.ps1"
$paths = Get-FeaturePathsEnv
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
$template = Join-Path $paths.REPO_ROOT 'templates/plan-template.md'
if (Test-Path $template) { Copy-Item $template $paths.IMPL_PLAN -Force }
if ($Json) {
[PSCustomObject]@{ FEATURE_SPEC=$paths.FEATURE_SPEC; IMPL_PLAN=$paths.IMPL_PLAN; SPECS_DIR=$paths.FEATURE_DIR; BRANCH=$paths.CURRENT_BRANCH } | ConvertTo-Json -Compress
} else {
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)"
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
}

View File

@@ -0,0 +1,91 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param([string]$AgentType)
$ErrorActionPreference = 'Stop'
$repoRoot = git rev-parse --show-toplevel
$currentBranch = git rev-parse --abbrev-ref HEAD
$featureDir = Join-Path $repoRoot "specs/$currentBranch"
$newPlan = Join-Path $featureDir 'plan.md'
if (-not (Test-Path $newPlan)) { Write-Error "ERROR: No plan.md found at $newPlan"; exit 1 }
$claudeFile = Join-Path $repoRoot 'CLAUDE.md'
$geminiFile = Join-Path $repoRoot 'GEMINI.md'
$copilotFile = Join-Path $repoRoot '.github/copilot-instructions.md'
Write-Output "=== Updating agent context files for feature $currentBranch ==="
function Get-PlanValue($pattern) {
if (-not (Test-Path $newPlan)) { return '' }
$line = Select-String -Path $newPlan -Pattern $pattern | Select-Object -First 1
if ($line) { return ($line.Line -replace "^\*\*$pattern\*\*: ", '') }
return ''
}
$newLang = Get-PlanValue 'Language/Version'
$newFramework = Get-PlanValue 'Primary Dependencies'
$newTesting = Get-PlanValue 'Testing'
$newDb = Get-PlanValue 'Storage'
$newProjectType = Get-PlanValue 'Project Type'
function Initialize-AgentFile($targetFile, $agentName) {
if (Test-Path $targetFile) { return }
$template = Join-Path $repoRoot 'templates/agent-file-template.md'
if (-not (Test-Path $template)) { Write-Error "Template not found: $template"; return }
$content = Get-Content $template -Raw
$content = $content.Replace('[PROJECT NAME]', (Split-Path $repoRoot -Leaf))
$content = $content.Replace('[DATE]', (Get-Date -Format 'yyyy-MM-dd'))
$content = $content.Replace('[EXTRACTED FROM ALL PLAN.MD FILES]', "- $newLang + $newFramework ($currentBranch)")
if ($newProjectType -match 'web') { $structure = "backend/`nfrontend/`ntests/" } else { $structure = "src/`ntests/" }
$content = $content.Replace('[ACTUAL STRUCTURE FROM PLANS]', $structure)
if ($newLang -match 'Python') { $commands = 'cd src && pytest && ruff check .' }
elseif ($newLang -match 'Rust') { $commands = 'cargo test && cargo clippy' }
elseif ($newLang -match 'JavaScript|TypeScript') { $commands = 'npm test && npm run lint' }
else { $commands = "# Add commands for $newLang" }
$content = $content.Replace('[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]', $commands)
$content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "$newLang: Follow standard conventions")
$content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- $currentBranch: Added $newLang + $newFramework")
$content | Set-Content $targetFile -Encoding UTF8
}
function Update-AgentFile($targetFile, $agentName) {
if (-not (Test-Path $targetFile)) { Initialize-AgentFile $targetFile $agentName; return }
$content = Get-Content $targetFile -Raw
if ($newLang -and ($content -notmatch [regex]::Escape($newLang))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newLang + $newFramework ($currentBranch)`n" }
if ($newDb -and $newDb -ne 'N/A' -and ($content -notmatch [regex]::Escape($newDb))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newDb ($currentBranch)`n" }
if ($content -match '## Recent Changes\n([\s\S]*?)(\n\n|$)') {
$changesBlock = $matches[1].Trim().Split("`n")
$changesBlock = ,"- $currentBranch: Added $newLang + $newFramework" + $changesBlock
$changesBlock = $changesBlock | Where-Object { $_ } | Select-Object -First 3
$joined = ($changesBlock -join "`n")
$content = [regex]::Replace($content, '## Recent Changes\n([\s\S]*?)(\n\n|$)', "## Recent Changes`n$joined`n`n")
}
$content = [regex]::Replace($content, 'Last updated: \d{4}-\d{2}-\d{2}', "Last updated: $(Get-Date -Format 'yyyy-MM-dd')")
$content | Set-Content $targetFile -Encoding UTF8
Write-Output "$agentName context file updated successfully"
}
switch ($AgentType) {
'claude' { Update-AgentFile $claudeFile 'Claude Code' }
'gemini' { Update-AgentFile $geminiFile 'Gemini CLI' }
'copilot' { Update-AgentFile $copilotFile 'GitHub Copilot' }
'' {
foreach ($pair in @(@{file=$claudeFile; name='Claude Code'}, @{file=$geminiFile; name='Gemini CLI'}, @{file=$copilotFile; name='GitHub Copilot'})) {
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
}
if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile)) {
Write-Output 'No agent context files found. Creating Claude Code context file by default.'
Update-AgentFile $claudeFile 'Claude Code'
}
}
Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, or leave empty for all."; exit 1 }
}
Write-Output ''
Write-Output 'Summary of changes:'
if ($newLang) { Write-Output "- Added language: $newLang" }
if ($newFramework) { Write-Output "- Added framework: $newFramework" }
if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" }
Write-Output ''
Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot]'

View File

@@ -1,44 +0,0 @@
#!/usr/bin/env bash
# Setup implementation plan structure for current branch
# Returns paths needed for implementation plan generation
# Usage: ./setup-plan.sh [--json]
set -e
JSON_MODE=false
for arg in "$@"; do
case "$arg" in
--json) JSON_MODE=true ;;
--help|-h) echo "Usage: $0 [--json]"; exit 0 ;;
esac
done
# Source common functions
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths
eval $(get_feature_paths)
# Check if on feature branch
check_feature_branch "$CURRENT_BRANCH" || exit 1
# Create specs directory if it doesn't exist
mkdir -p "$FEATURE_DIR"
# Copy plan template if it exists
TEMPLATE="$REPO_ROOT/templates/plan-template.md"
if [ -f "$TEMPLATE" ]; then
cp "$TEMPLATE" "$IMPL_PLAN"
fi
if $JSON_MODE; then
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH"
else
# Output all paths for LLM use
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "SPECS_DIR: $FEATURE_DIR"
echo "BRANCH: $CURRENT_BRANCH"
fi

View File

@@ -1,234 +0,0 @@
#!/usr/bin/env bash
# Incrementally update agent context files based on new feature plan
# Supports: CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md
# O(1) operation - only reads current context file and new plan.md
set -e
REPO_ROOT=$(git rev-parse --show-toplevel)
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
NEW_PLAN="$FEATURE_DIR/plan.md"
# Determine which agent context files to update
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
# Allow override via argument
AGENT_TYPE="$1"
if [ ! -f "$NEW_PLAN" ]; then
echo "ERROR: No plan.md found at $NEW_PLAN"
exit 1
fi
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
# Extract tech from new plan
NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_TESTING=$(grep "^**Testing**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Testing**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "")
# Function to update a single agent context file
update_agent_file() {
local target_file="$1"
local agent_name="$2"
echo "Updating $agent_name context file: $target_file"
# Create temp file for new context
local temp_file=$(mktemp)
# If file doesn't exist, create from template
if [ ! -f "$target_file" ]; then
echo "Creating new $agent_name context file..."
# Check if this is the SDD repo itself
if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then
cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file"
else
echo "ERROR: Template not found at $REPO_ROOT/templates/agent-file-template.md"
return 1
fi
# Replace placeholders
sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file"
sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file"
sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file"
# Add project structure based on type
if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then
sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file"
else
sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file"
fi
# Add minimal commands
if [[ "$NEW_LANG" == *"Python"* ]]; then
COMMANDS="cd src && pytest && ruff check ."
elif [[ "$NEW_LANG" == *"Rust"* ]]; then
COMMANDS="cargo test && cargo clippy"
elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then
COMMANDS="npm test && npm run lint"
else
COMMANDS="# Add commands for $NEW_LANG"
fi
sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file"
# Add code style
sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"
# Add recent changes
sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file"
rm "$temp_file.bak"
else
echo "Updating existing $agent_name context file..."
# Extract manual additions
local manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1)
local manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$target_file" | cut -d: -f1)
if [ ! -z "$manual_start" ] && [ ! -z "$manual_end" ]; then
sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt
fi
# Parse existing file and create updated version
python3 - << EOF
import re
import sys
from datetime import datetime
# Read existing file
with open("$target_file", 'r') as f:
content = f.read()
# Check if new tech already exists
tech_section = re.search(r'## Active Technologies\n(.*?)\n\n', content, re.DOTALL)
if tech_section:
existing_tech = tech_section.group(1)
# Add new tech if not already present
new_additions = []
if "$NEW_LANG" and "$NEW_LANG" not in existing_tech:
new_additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)")
if "$NEW_DB" and "$NEW_DB" not in existing_tech and "$NEW_DB" != "N/A":
new_additions.append(f"- $NEW_DB ($CURRENT_BRANCH)")
if new_additions:
updated_tech = existing_tech + "\n" + "\n".join(new_additions)
content = content.replace(tech_section.group(0), f"## Active Technologies\n{updated_tech}\n\n")
# Update project structure if needed
if "$NEW_PROJECT_TYPE" == "web" and "frontend/" not in content:
struct_section = re.search(r'## Project Structure\n\`\`\`\n(.*?)\n\`\`\`', content, re.DOTALL)
if struct_section:
updated_struct = struct_section.group(1) + "\nfrontend/src/ # Web UI"
content = re.sub(r'(## Project Structure\n\`\`\`\n).*?(\n\`\`\`)',
f'\\1{updated_struct}\\2', content, flags=re.DOTALL)
# Add new commands if language is new
if "$NEW_LANG" and f"# {NEW_LANG}" not in content:
commands_section = re.search(r'## Commands\n\`\`\`bash\n(.*?)\n\`\`\`', content, re.DOTALL)
if not commands_section:
commands_section = re.search(r'## Commands\n(.*?)\n\n', content, re.DOTALL)
if commands_section:
new_commands = commands_section.group(1)
if "Python" in "$NEW_LANG":
new_commands += "\ncd src && pytest && ruff check ."
elif "Rust" in "$NEW_LANG":
new_commands += "\ncargo test && cargo clippy"
elif "JavaScript" in "$NEW_LANG" or "TypeScript" in "$NEW_LANG":
new_commands += "\nnpm test && npm run lint"
if "```bash" in content:
content = re.sub(r'(## Commands\n\`\`\`bash\n).*?(\n\`\`\`)',
f'\\1{new_commands}\\2', content, flags=re.DOTALL)
else:
content = re.sub(r'(## Commands\n).*?(\n\n)',
f'\\1{new_commands}\\2', content, flags=re.DOTALL)
# Update recent changes (keep only last 3)
changes_section = re.search(r'## Recent Changes\n(.*?)(\n\n|$)', content, re.DOTALL)
if changes_section:
changes = changes_section.group(1).strip().split('\n')
changes.insert(0, f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK")
# Keep only last 3
changes = changes[:3]
content = re.sub(r'(## Recent Changes\n).*?(\n\n|$)',
f'\\1{chr(10).join(changes)}\\2', content, flags=re.DOTALL)
# Update date
content = re.sub(r'Last updated: \d{4}-\d{2}-\d{2}',
f'Last updated: {datetime.now().strftime("%Y-%m-%d")}', content)
# Write to temp file
with open("$temp_file", 'w') as f:
f.write(content)
EOF
# Restore manual additions if they exist
if [ -f /tmp/manual_additions.txt ]; then
# Remove old manual section from temp file
sed -i.bak '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/d' "$temp_file"
# Append manual additions
cat /tmp/manual_additions.txt >> "$temp_file"
rm /tmp/manual_additions.txt "$temp_file.bak"
fi
fi
# Move temp file to final location
mv "$temp_file" "$target_file"
echo "$agent_name context file updated successfully"
}
# Update files based on argument or detect existing files
case "$AGENT_TYPE" in
"claude")
update_agent_file "$CLAUDE_FILE" "Claude Code"
;;
"gemini")
update_agent_file "$GEMINI_FILE" "Gemini CLI"
;;
"copilot")
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
;;
"")
# Update all existing files
[ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"
[ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"
[ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"
# If no files exist, create based on current directory or ask user
if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then
echo "No agent context files found. Creating Claude Code context file by default."
update_agent_file "$CLAUDE_FILE" "Claude Code"
fi
;;
*)
echo "ERROR: Unknown agent type '$AGENT_TYPE'. Use: claude, gemini, copilot, or leave empty for all."
exit 1
;;
esac
echo ""
echo "Summary of changes:"
if [ ! -z "$NEW_LANG" ]; then
echo "- Added language: $NEW_LANG"
fi
if [ ! -z "$NEW_FRAMEWORK" ]; then
echo "- Added framework: $NEW_FRAMEWORK"
fi
if [ ! -z "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ]; then
echo "- Added database: $NEW_DB"
fi
echo ""
echo "Usage: $0 [claude|gemini|copilot]"
echo " - No argument: Update all existing agent context files"
echo " - claude: Update only CLAUDE.md"
echo " - gemini: Update only GEMINI.md"
echo " - copilot: Update only .github/copilot-instructions.md"

View File

@@ -30,7 +30,7 @@ import tempfile
import shutil import shutil
import json import json
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional, Tuple
import typer import typer
import httpx import httpx
@@ -58,6 +58,11 @@ AI_CHOICES = {
"claude": "Claude Code", "claude": "Claude Code",
"gemini": "Gemini CLI" "gemini": "Gemini CLI"
} }
# Add script type choices
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
# Claude CLI local installation path after migrate-installer
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
# ASCII Art Banner # ASCII Art Banner
BANNER = """ BANNER = """
@@ -335,8 +340,28 @@ def run_command(cmd: list[str], check_return: bool = True, capture: bool = False
return None return None
def check_tool_for_tracker(tool: str, install_hint: str, tracker: StepTracker) -> bool:
"""Check if a tool is installed and update tracker."""
if shutil.which(tool):
tracker.complete(tool, "available")
return True
else:
tracker.error(tool, f"not found - {install_hint}")
return False
def check_tool(tool: str, install_hint: str) -> bool: def check_tool(tool: str, install_hint: str) -> bool:
"""Check if a tool is installed.""" """Check if a tool is installed."""
# Special handling for Claude CLI after `claude migrate-installer`
# See: https://github.com/github/spec-kit/issues/123
# The migrate-installer command REMOVES the original executable from PATH
# and creates an alias at ~/.claude/local/claude instead
# This path should be prioritized over other claude executables in PATH
if tool == "claude":
if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file():
return True
if shutil.which(tool): if shutil.which(tool):
return True return True
else: else:
@@ -390,7 +415,7 @@ def init_git_repo(project_path: Path, quiet: bool = False) -> bool:
os.chdir(original_cwd) os.chdir(original_cwd)
def download_template_from_github(ai_assistant: str, download_dir: Path, *, verbose: bool = True, show_progress: bool = True, client: httpx.Client = None): def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False) -> Tuple[Path, dict]:
repo_owner = "github" repo_owner = "github"
repo_name = "spec-kit" repo_name = "spec-kit"
if client is None: if client is None:
@@ -402,26 +427,32 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
try: try:
response = client.get(api_url, timeout=30, follow_redirects=True) response = client.get(api_url, timeout=30, follow_redirects=True)
response.raise_for_status() status = response.status_code
release_data = response.json() if status != 200:
except httpx.RequestError as e: msg = f"GitHub API returned {status} for {api_url}"
if verbose: if debug:
console.print(f"[red]Error fetching release information:[/red] {e}") msg += f"\nResponse headers: {response.headers}\nBody (truncated 500): {response.text[:500]}"
raise RuntimeError(msg)
try:
release_data = response.json()
except ValueError as je:
raise RuntimeError(f"Failed to parse release JSON: {je}\nRaw (truncated 400): {response.text[:400]}")
except Exception as e:
console.print(f"[red]Error fetching release information[/red]")
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
raise typer.Exit(1) raise typer.Exit(1)
# Find the template asset for the specified AI assistant # Find the template asset for the specified AI assistant
pattern = f"spec-kit-template-{ai_assistant}" pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
matching_assets = [ matching_assets = [
asset for asset in release_data.get("assets", []) asset for asset in release_data.get("assets", [])
if pattern in asset["name"] and asset["name"].endswith(".zip") if pattern in asset["name"] and asset["name"].endswith(".zip")
] ]
if not matching_assets: if not matching_assets:
if verbose: console.print(f"[red]No matching release asset found[/red] for pattern: [bold]{pattern}[/bold]")
console.print(f"[red]Error:[/red] No template found for AI assistant '{ai_assistant}'") asset_names = [a.get('name','?') for a in release_data.get('assets', [])]
console.print(f"[yellow]Available assets:[/yellow]") console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow"))
for asset in release_data.get("assets", []):
console.print(f" - {asset['name']}")
raise typer.Exit(1) raise typer.Exit(1)
# Use the first matching asset # Use the first matching asset
@@ -441,8 +472,10 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
console.print(f"[cyan]Downloading template...[/cyan]") console.print(f"[cyan]Downloading template...[/cyan]")
try: try:
with client.stream("GET", download_url, timeout=30, follow_redirects=True) as response: with client.stream("GET", download_url, timeout=60, follow_redirects=True) as response:
response.raise_for_status() if response.status_code != 200:
body_sample = response.text[:400]
raise RuntimeError(f"Download failed with {response.status_code}\nHeaders: {response.headers}\nBody (truncated): {body_sample}")
total_size = int(response.headers.get('content-length', 0)) total_size = int(response.headers.get('content-length', 0))
with open(zip_path, 'wb') as f: with open(zip_path, 'wb') as f:
if total_size == 0: if total_size == 0:
@@ -465,11 +498,12 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
else: else:
for chunk in response.iter_bytes(chunk_size=8192): for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk) f.write(chunk)
except httpx.RequestError as e: except Exception as e:
if verbose: console.print(f"[red]Error downloading template[/red]")
console.print(f"[red]Error downloading template:[/red] {e}") detail = str(e)
if zip_path.exists(): if zip_path.exists():
zip_path.unlink() zip_path.unlink()
console.print(Panel(detail, title="Download Error", border_style="red"))
raise typer.Exit(1) raise typer.Exit(1)
if verbose: if verbose:
console.print(f"Downloaded: {filename}") console.print(f"Downloaded: {filename}")
@@ -482,7 +516,7 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, verb
return zip_path, metadata return zip_path, metadata
def download_and_extract_template(project_path: Path, ai_assistant: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None) -> Path: def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False) -> Path:
"""Download the latest release and extract it to create a new project. """Download the latest release and extract it to create a new project.
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup) Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
""" """
@@ -495,9 +529,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
zip_path, meta = download_template_from_github( zip_path, meta = download_template_from_github(
ai_assistant, ai_assistant,
current_dir, current_dir,
script_type=script_type,
verbose=verbose and tracker is None, verbose=verbose and tracker is None,
show_progress=(tracker is None), show_progress=(tracker is None),
client=client client=client,
debug=debug
) )
if tracker: if tracker:
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)") tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
@@ -614,6 +650,8 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
else: else:
if verbose: if verbose:
console.print(f"[red]Error extracting template:[/red] {e}") console.print(f"[red]Error extracting template:[/red] {e}")
if debug:
console.print(Panel(str(e), title="Extraction Error", border_style="red"))
# Clean up project directory if created and not current directory # Clean up project directory if created and not current directory
if not is_current_dir and project_path.exists(): if not is_current_dir and project_path.exists():
shutil.rmtree(project_path) shutil.rmtree(project_path)
@@ -636,60 +674,44 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, is_curr
def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None: def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None:
"""Ensure POSIX .sh scripts in the project scripts directory have execute bits (no-op on Windows).""" """Ensure POSIX .sh scripts under .specify/scripts (recursively) have execute bits (no-op on Windows)."""
if os.name == "nt": if os.name == "nt":
return # Windows: skip silently return # Windows: skip silently
scripts_dir = project_path / "scripts" scripts_root = project_path / ".specify" / "scripts"
if not scripts_dir.is_dir(): if not scripts_root.is_dir():
return return
failures: list[str] = [] failures: list[str] = []
updated = 0 updated = 0
for script in scripts_dir.glob("*.sh"): for script in scripts_root.rglob("*.sh"):
try: try:
# Skip symlinks if script.is_symlink() or not script.is_file():
if script.is_symlink():
continue continue
# Must be a regular file
if not script.is_file():
continue
# Quick shebang check
try: try:
with script.open("rb") as f: with script.open("rb") as f:
first_two = f.read(2) if f.read(2) != b"#!":
if first_two != b"#!": continue
continue
except Exception: except Exception:
continue continue
st = script.stat() st = script.stat(); mode = st.st_mode
mode = st.st_mode
# If already any execute bit set, skip
if mode & 0o111: if mode & 0o111:
continue continue
# Only add execute bits that correspond to existing read bits
new_mode = mode new_mode = mode
if mode & 0o400: # owner read if mode & 0o400: new_mode |= 0o100
new_mode |= 0o100 if mode & 0o040: new_mode |= 0o010
if mode & 0o040: # group read if mode & 0o004: new_mode |= 0o001
new_mode |= 0o010
if mode & 0o004: # other read
new_mode |= 0o001
# Fallback: ensure at least owner execute
if not (new_mode & 0o100): if not (new_mode & 0o100):
new_mode |= 0o100 new_mode |= 0o100
os.chmod(script, new_mode) os.chmod(script, new_mode)
updated += 1 updated += 1
except Exception as e: except Exception as e:
failures.append(f"{script.name}: {e}") failures.append(f"{script.relative_to(scripts_root)}: {e}")
if tracker: if tracker:
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "") detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
tracker.add("chmod", "Set script permissions") tracker.add("chmod", "Set script permissions recursively")
if failures: (tracker.error if failures else tracker.complete)("chmod", detail)
tracker.error("chmod", detail)
else:
tracker.complete("chmod", detail)
else: else:
if updated: if updated:
console.print(f"[cyan]Updated execute permissions on {updated} script(s)[/cyan]") console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]")
if failures: if failures:
console.print("[yellow]Some scripts could not be updated:[/yellow]") console.print("[yellow]Some scripts could not be updated:[/yellow]")
for f in failures: for f in failures:
@@ -700,10 +722,12 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
def init( def init(
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"), project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here)"),
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, or copilot"), ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, or copilot"),
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"), ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"), no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"), here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"), skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
): ):
""" """
Initialize a new Specify project from the latest template. Initialize a new Specify project from the latest template.
@@ -806,6 +830,24 @@ def init(
console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check") console.print("[yellow]Tip:[/yellow] Use --ignore-agent-tools to skip this check")
raise typer.Exit(1) raise typer.Exit(1)
# Determine script type (explicit, interactive, or OS default)
if script_type:
if script_type not in SCRIPT_TYPE_CHOICES:
console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}")
raise typer.Exit(1)
selected_script = script_type
else:
# Auto-detect default
default_script = "ps" if os.name == "nt" else "sh"
# Provide interactive selection similar to AI if stdin is a TTY
if sys.stdin.isatty():
selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script)
else:
selected_script = default_script
console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}")
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
# Download and set up project # Download and set up project
# New tree-based progress (no emojis); include earlier substeps # New tree-based progress (no emojis); include earlier substeps
tracker = StepTracker("Initialize Specify Project") tracker = StepTracker("Initialize Specify Project")
@@ -816,6 +858,8 @@ def init(
tracker.complete("precheck", "ok") tracker.complete("precheck", "ok")
tracker.add("ai-select", "Select AI assistant") tracker.add("ai-select", "Select AI assistant")
tracker.complete("ai-select", f"{selected_ai}") tracker.complete("ai-select", f"{selected_ai}")
tracker.add("script-select", "Select script type")
tracker.complete("script-select", selected_script)
for key, label in [ for key, label in [
("fetch", "Fetch latest release"), ("fetch", "Fetch latest release"),
("download", "Download template"), ("download", "Download template"),
@@ -838,7 +882,7 @@ def init(
local_ssl_context = ssl_context if verify else False local_ssl_context = ssl_context if verify else False
local_client = httpx.Client(verify=local_ssl_context) local_client = httpx.Client(verify=local_ssl_context)
download_and_extract_template(project_path, selected_ai, here, verbose=False, tracker=tracker, client=local_client) download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug)
# Ensure scripts are executable (POSIX) # Ensure scripts are executable (POSIX)
ensure_executable_scripts(project_path, tracker=tracker) ensure_executable_scripts(project_path, tracker=tracker)
@@ -861,6 +905,16 @@ def init(
tracker.complete("final", "project ready") tracker.complete("final", "project ready")
except Exception as e: except Exception as e:
tracker.error("final", str(e)) tracker.error("final", str(e))
console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red"))
if debug:
_env_pairs = [
("Python", sys.version.split()[0]),
("Platform", sys.platform),
("CWD", str(Path.cwd())),
]
_label_width = max(len(k) for k, _ in _env_pairs)
env_lines = [f"{k.ljust(_label_width)} → [bright_black]{v}[/bright_black]" for k, v in _env_pairs]
console.print(Panel("\n".join(env_lines), title="Debug Environment", border_style="magenta"))
if not here and project_path.exists(): if not here and project_path.exists():
shutil.rmtree(project_path) shutil.rmtree(project_path)
raise typer.Exit(1) raise typer.Exit(1)
@@ -896,6 +950,7 @@ def init(
elif selected_ai == "copilot": elif selected_ai == "copilot":
steps_lines.append(f"{step_num}. Open in Visual Studio Code and use [bold cyan]/specify[/], [bold cyan]/plan[/], [bold cyan]/tasks[/] commands with GitHub Copilot") steps_lines.append(f"{step_num}. Open in Visual Studio Code and use [bold cyan]/specify[/], [bold cyan]/plan[/], [bold cyan]/tasks[/] commands with GitHub Copilot")
# Removed script variant step (scripts are transparent to users)
step_num += 1 step_num += 1
steps_lines.append(f"{step_num}. Update [bold magenta]CONSTITUTION.md[/bold magenta] with your project's non-negotiable principles") steps_lines.append(f"{step_num}. Update [bold magenta]CONSTITUTION.md[/bold magenta] with your project's non-negotiable principles")
@@ -906,37 +961,36 @@ def init(
# Removed farewell line per user request # Removed farewell line per user request
# Add skip_tls option to check
@app.command() @app.command()
def check(skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)")): def check():
"""Check that all required tools are installed.""" """Check that all required tools are installed."""
show_banner() show_banner()
console.print("[bold]Checking Specify requirements...[/bold]\n") console.print("[bold]Checking for installed tools...[/bold]\n")
# Check if we have internet connectivity by trying to reach GitHub API # Create tracker for checking tools
console.print("[cyan]Checking internet connectivity...[/cyan]") tracker = StepTracker("Check Available Tools")
verify = not skip_tls
local_ssl_context = ssl_context if verify else False
local_client = httpx.Client(verify=local_ssl_context)
try:
response = local_client.get("https://api.github.com", timeout=5, follow_redirects=True)
console.print("[green]✓[/green] Internet connection available")
except httpx.RequestError:
console.print("[red]✗[/red] No internet connection - required for downloading templates")
console.print("[yellow]Please check your internet connection[/yellow]")
console.print("\n[cyan]Optional tools:[/cyan]") # Add all tools we want to check
git_ok = check_tool("git", "https://git-scm.com/downloads") tracker.add("git", "Git version control")
tracker.add("claude", "Claude Code CLI")
tracker.add("gemini", "Gemini CLI")
console.print("\n[cyan]Optional AI tools:[/cyan]") # Check each tool
claude_ok = check_tool("claude", "Install from: https://docs.anthropic.com/en/docs/claude-code/setup") git_ok = check_tool_for_tracker("git", "https://git-scm.com/downloads", tracker)
gemini_ok = check_tool("gemini", "Install from: https://github.com/google-gemini/gemini-cli") claude_ok = check_tool_for_tracker("claude", "https://docs.anthropic.com/en/docs/claude-code/setup", tracker)
gemini_ok = check_tool_for_tracker("gemini", "https://github.com/google-gemini/gemini-cli", tracker)
console.print("\n[green]✓ Specify CLI is ready to use![/green]") # Render the final tree
console.print(tracker.render())
# Summary
console.print("\n[bold green]Specify CLI is ready to use![/bold green]")
# Recommendations
if not git_ok: if not git_ok:
console.print("[yellow]Consider installing git for repository management[/yellow]") console.print("[dim]Tip: Install git for repository management[/dim]")
if not (claude_ok or gemini_ok): if not (claude_ok or gemini_ok):
console.print("[yellow]Consider installing an AI assistant for the best experience[/yellow]") console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
def main(): def main():

View File

@@ -1,15 +1,12 @@
--- ---
name: plan description: Execute the implementation planning workflow using the plan template to generate design artifacts.
description: "Plan how to implement the specified feature. This is the second step in the Spec-Driven Development lifecycle."
--- ---
<!-- VARIANT:sh 1. Run `scripts/bash/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. -->
Plan how to implement the specified feature. <!-- VARIANT:ps 1. Run `scripts/powershell/setup-plan.ps1 -Json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. -->
This is the second step in the Spec-Driven Development lifecycle.
Given the implementation details provided as an argument, do this: Given the implementation details provided as an argument, do this:
1. Run `scripts/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. 1. VARIANT-INJECT
2. Read and analyze the feature specification to understand: 2. Read and analyze the feature specification to understand:
- The feature requirements and user stories - The feature requirements and user stories
- Functional and non-functional requirements - Functional and non-functional requirements

View File

@@ -1,15 +1,12 @@
--- ---
name: specify description: Create or update the feature specification from a natural language feature description.
description: "Start a new feature by creating a specification and feature branch. This is the first step in the Spec-Driven Development lifecycle."
--- ---
<!-- VARIANT:sh 1. Run the script `scripts/bash/create-new-feature.sh --json "{ARGS}"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. -->
Start a new feature by creating a specification and feature branch. <!-- VARIANT:ps 1. Run the script `scripts/powershell/create-new-feature.ps1 -Json "{ARGS}"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. -->
This is the first step in the Spec-Driven Development lifecycle.
Given the feature description provided as an argument, do this: Given the feature description provided as an argument, do this:
1. Run the script `scripts/create-new-feature.sh --json "{ARGS}"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. 1. VARIANT-INJECT
2. Load `templates/spec-template.md` to understand required sections. 2. Load `templates/spec-template.md` to understand required sections.
3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. 3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
4. Report completion with branch name, spec file path, and readiness for the next phase. 4. Report completion with branch name, spec file path, and readiness for the next phase.

View File

@@ -1,15 +1,12 @@
--- ---
name: tasks description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
description: "Break down the plan into executable tasks. This is the third step in the Spec-Driven Development lifecycle."
--- ---
<!-- VARIANT:sh 1. Run `scripts/bash/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. -->
Break down the plan into executable tasks. <!-- VARIANT:ps 1. Run `scripts/powershell/check-task-prerequisites.ps1 -Json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. -->
This is the third step in the Spec-Driven Development lifecycle.
Given the context provided as an argument, do this: Given the context provided as an argument, do this:
1. Run `scripts/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. 1. VARIANT-INJECT
2. Load and analyze available design documents: 2. Load and analyze available design documents:
- Always read plan.md for tech stack and libraries - Always read plan.md for tech stack and libraries
- IF EXISTS: Read data-model.md for entities - IF EXISTS: Read data-model.md for entities

View File

@@ -1,5 +1,8 @@
# Implementation Plan: [FEATURE] # Implementation Plan: [FEATURE]
<!-- VARIANT:sh - Run `/scripts/bash/update-agent-context.sh __AGENT__` for your AI assistant -->
<!-- VARIANT:ps - Run `/scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__` for your AI assistant -->
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] **Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` **Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
@@ -171,7 +174,7 @@ ios/ or android/
- Quickstart test = story validation steps - Quickstart test = story validation steps
5. **Update agent file incrementally** (O(1) operation): 5. **Update agent file incrementally** (O(1) operation):
- Run `/scripts/update-agent-context.sh [claude|gemini|copilot]` for your AI assistant VARIANT-INJECT
- If exists: Add only NEW tech from current plan - If exists: Add only NEW tech from current plan
- Preserve manual additions between markers - Preserve manual additions between markers
- Update recent changes (keep last 3) - Update recent changes (keep last 3)