mirror of
https://github.com/github/spec-kit.git
synced 2026-04-03 03:03:09 +00:00
Compare commits
1 Commits
copilot/re
...
chore/rele
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4fe2d35a9 |
@@ -8,15 +8,15 @@ run_command() {
|
|||||||
local command_to_run="$*"
|
local command_to_run="$*"
|
||||||
local output
|
local output
|
||||||
local exit_code
|
local exit_code
|
||||||
|
|
||||||
# Capture all output (stdout and stderr)
|
# Capture all output (stdout and stderr)
|
||||||
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
|
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
|
||||||
exit_code=${exit_code:-0}
|
exit_code=${exit_code:-0}
|
||||||
|
|
||||||
if [ $exit_code -ne 0 ]; then
|
if [ $exit_code -ne 0 ]; then
|
||||||
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
|
echo -e "\033[0;31m[ERROR] Command failed (Exit Code $exit_code): $command_to_run\033[0m" >&2
|
||||||
echo -e "\033[0;31m$output\033[0m" >&2
|
echo -e "\033[0;31m$output\033[0m" >&2
|
||||||
|
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -51,17 +51,9 @@ echo -e "\n🤖 Installing OpenCode CLI..."
|
|||||||
run_command "npm install -g opencode-ai@latest"
|
run_command "npm install -g opencode-ai@latest"
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|
||||||
echo -e "\n🤖 Installing Junie CLI..."
|
|
||||||
run_command "npm install -g @jetbrains/junie-cli@latest"
|
|
||||||
echo "✅ Done"
|
|
||||||
|
|
||||||
echo -e "\n🤖 Installing Pi Coding Agent..."
|
|
||||||
run_command "npm install -g @mariozechner/pi-coding-agent@latest"
|
|
||||||
echo "✅ Done"
|
|
||||||
|
|
||||||
echo -e "\n🤖 Installing Kiro CLI..."
|
echo -e "\n🤖 Installing Kiro CLI..."
|
||||||
# https://kiro.dev/docs/cli/
|
# https://kiro.dev/docs/cli/
|
||||||
KIRO_INSTALLER_URL="https://kiro.dev/install.sh"
|
KIRO_INSTALLER_URL="https://cli.kiro.dev/install"
|
||||||
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
KIRO_INSTALLER_SHA256="7487a65cf310b7fb59b357c4b5e6e3f3259d383f4394ecedb39acf70f307cffb"
|
||||||
KIRO_INSTALLER_PATH="$(mktemp)"
|
KIRO_INSTALLER_PATH="$(mktemp)"
|
||||||
|
|
||||||
@@ -88,11 +80,6 @@ fi
|
|||||||
run_command "$kiro_binary --help > /dev/null"
|
run_command "$kiro_binary --help > /dev/null"
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|
||||||
echo -e "\n🤖 Installing Kimi CLI..."
|
|
||||||
# https://code.kimi.com
|
|
||||||
run_command "pipx install kimi-cli"
|
|
||||||
echo "✅ Done"
|
|
||||||
|
|
||||||
echo -e "\n🤖 Installing CodeBuddy CLI..."
|
echo -e "\n🤖 Installing CodeBuddy CLI..."
|
||||||
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
2
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
@@ -8,7 +8,7 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
|
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
|
||||||
|
|
||||||
**Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, Tabnine CLI, Antigravity, IBM Bob, Mistral Vibe, Kimi Code, Trae, Pi Coding Agent, iFlow CLI
|
**Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, IBM Bob, Antigravity
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: agent-name
|
id: agent-name
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,7 +7,7 @@ contact_links:
|
|||||||
url: https://github.com/github/spec-kit/blob/main/README.md
|
url: https://github.com/github/spec-kit/blob/main/README.md
|
||||||
about: Read the Spec Kit documentation and guides
|
about: Read the Spec Kit documentation and guides
|
||||||
- name: 🛠️ Extension Development Guide
|
- name: 🛠️ Extension Development Guide
|
||||||
url: https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-DEVELOPMENT-GUIDE.md
|
url: https://github.com/manfredseee/spec-kit/blob/main/extensions/EXTENSION-DEVELOPMENT-GUIDE.md
|
||||||
about: Learn how to develop and publish Spec Kit extensions
|
about: Learn how to develop and publish Spec Kit extensions
|
||||||
- name: 🤝 Contributing Guide
|
- name: 🤝 Contributing Guide
|
||||||
url: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md
|
url: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ body:
|
|||||||
- Review the [Extension Publishing Guide](https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-PUBLISHING-GUIDE.md)
|
- Review the [Extension Publishing Guide](https://github.com/github/spec-kit/blob/main/extensions/EXTENSION-PUBLISHING-GUIDE.md)
|
||||||
- Ensure your extension has a valid `extension.yml` manifest
|
- Ensure your extension has a valid `extension.yml` manifest
|
||||||
- Create a GitHub release with a version tag (e.g., v1.0.0)
|
- Create a GitHub release with a version tag (e.g., v1.0.0)
|
||||||
- Test installation: `specify extension add <extension-name> --from <your-release-url>`
|
- Test installation: `specify extension add --from <your-release-url>`
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: extension-id
|
id: extension-id
|
||||||
@@ -229,7 +229,7 @@ body:
|
|||||||
placeholder: |
|
placeholder: |
|
||||||
```bash
|
```bash
|
||||||
# Install extension
|
# Install extension
|
||||||
specify extension add <extension-name> --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
|
|
||||||
# Use a command
|
# Use a command
|
||||||
/speckit.your-extension.command-name arg1 arg2
|
/speckit.your-extension.command-name arg1 arg2
|
||||||
|
|||||||
169
.github/ISSUE_TEMPLATE/preset_submission.yml
vendored
169
.github/ISSUE_TEMPLATE/preset_submission.yml
vendored
@@ -1,169 +0,0 @@
|
|||||||
name: Preset Submission
|
|
||||||
description: Submit your preset to the Spec Kit preset catalog
|
|
||||||
title: "[Preset]: Add "
|
|
||||||
labels: ["preset-submission", "enhancement", "needs-triage"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for contributing a preset! This template helps you submit your preset to the community catalog.
|
|
||||||
|
|
||||||
**Before submitting:**
|
|
||||||
- Review the [Preset Publishing Guide](https://github.com/github/spec-kit/blob/main/presets/PUBLISHING.md)
|
|
||||||
- Ensure your preset has a valid `preset.yml` manifest
|
|
||||||
- Create a GitHub release with a version tag (e.g., v1.0.0)
|
|
||||||
- Test installation from the release archive: `specify preset add --from <download-url>`
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: preset-id
|
|
||||||
attributes:
|
|
||||||
label: Preset ID
|
|
||||||
description: Unique preset identifier (lowercase with hyphens only)
|
|
||||||
placeholder: "e.g., healthcare-compliance"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: preset-name
|
|
||||||
attributes:
|
|
||||||
label: Preset Name
|
|
||||||
description: Human-readable preset name
|
|
||||||
placeholder: "e.g., Healthcare Compliance"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
description: Semantic version number
|
|
||||||
placeholder: "e.g., 1.0.0"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: description
|
|
||||||
attributes:
|
|
||||||
label: Description
|
|
||||||
description: Brief description of what your preset does (under 200 characters)
|
|
||||||
placeholder: Enforces HIPAA-compliant spec workflows with audit templates and compliance checklists
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: author
|
|
||||||
attributes:
|
|
||||||
label: Author
|
|
||||||
description: Your name or organization
|
|
||||||
placeholder: "e.g., John Doe or Acme Corp"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: repository
|
|
||||||
attributes:
|
|
||||||
label: Repository URL
|
|
||||||
description: GitHub repository URL for your preset
|
|
||||||
placeholder: "https://github.com/your-org/spec-kit-your-preset"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: download-url
|
|
||||||
attributes:
|
|
||||||
label: Download URL
|
|
||||||
description: URL to the GitHub release archive for your preset (e.g., https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip)
|
|
||||||
placeholder: "https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: license
|
|
||||||
attributes:
|
|
||||||
label: License
|
|
||||||
description: Open source license type
|
|
||||||
placeholder: "e.g., MIT, Apache-2.0"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: speckit-version
|
|
||||||
attributes:
|
|
||||||
label: Required Spec Kit Version
|
|
||||||
description: Minimum Spec Kit version required
|
|
||||||
placeholder: "e.g., >=0.3.0"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: templates-provided
|
|
||||||
attributes:
|
|
||||||
label: Templates Provided
|
|
||||||
description: List the template overrides your preset provides
|
|
||||||
placeholder: |
|
|
||||||
- spec-template.md — adds compliance section
|
|
||||||
- plan-template.md — includes audit checkpoints
|
|
||||||
- checklist-template.md — HIPAA compliance checklist
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: commands-provided
|
|
||||||
attributes:
|
|
||||||
label: Commands Provided (optional)
|
|
||||||
description: List any command overrides your preset provides
|
|
||||||
placeholder: |
|
|
||||||
- speckit.specify.md — customized for compliance workflows
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: tags
|
|
||||||
attributes:
|
|
||||||
label: Tags
|
|
||||||
description: 2-5 relevant tags (lowercase, separated by commas)
|
|
||||||
placeholder: "compliance, healthcare, hipaa, audit"
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: features
|
|
||||||
attributes:
|
|
||||||
label: Key Features
|
|
||||||
description: List the main features and capabilities of your preset
|
|
||||||
placeholder: |
|
|
||||||
- HIPAA-compliant spec templates
|
|
||||||
- Audit trail checklists
|
|
||||||
- Compliance review workflow
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: testing
|
|
||||||
attributes:
|
|
||||||
label: Testing Checklist
|
|
||||||
description: Confirm that your preset has been tested
|
|
||||||
options:
|
|
||||||
- label: Preset installs successfully via `specify preset add`
|
|
||||||
required: true
|
|
||||||
- label: Template resolution works correctly after installation
|
|
||||||
required: true
|
|
||||||
- label: Documentation is complete and accurate
|
|
||||||
required: true
|
|
||||||
- label: Tested on at least one real project
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: checkboxes
|
|
||||||
id: requirements
|
|
||||||
attributes:
|
|
||||||
label: Submission Requirements
|
|
||||||
description: Verify your preset meets all requirements
|
|
||||||
options:
|
|
||||||
- label: Valid `preset.yml` manifest included
|
|
||||||
required: true
|
|
||||||
- label: README.md with description and usage instructions
|
|
||||||
required: true
|
|
||||||
- label: LICENSE file included
|
|
||||||
required: true
|
|
||||||
- label: GitHub release created with version tag
|
|
||||||
required: true
|
|
||||||
- label: Preset ID follows naming conventions (lowercase-with-hyphens)
|
|
||||||
required: true
|
|
||||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -64,5 +64,5 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v5
|
uses: actions/deploy-pages@v4
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Run markdownlint-cli2
|
- name: Run markdownlint-cli2
|
||||||
uses: DavidAnson/markdownlint-cli2-action@ce4853d43830c74c1753b39f3cf40f71c2031eb9 # v23
|
uses: DavidAnson/markdownlint-cli2-action@v19
|
||||||
with:
|
with:
|
||||||
globs: |
|
globs: |
|
||||||
'**/*.md'
|
'**/*.md'
|
||||||
|
|||||||
51
.github/workflows/release-trigger.yml
vendored
51
.github/workflows/release-trigger.yml
vendored
@@ -86,10 +86,8 @@ jobs:
|
|||||||
if [ -f "CHANGELOG.md" ]; then
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
DATE=$(date +%Y-%m-%d)
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
# Get the previous tag by sorting all version tags numerically
|
# Get the previous tag to compare commits
|
||||||
# (git describe --tags only finds tags reachable from HEAD,
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||||
# which misses tags on unmerged release branches)
|
|
||||||
PREVIOUS_TAG=$(git tag -l 'v*' --sort=-version:refname | head -n 1)
|
|
||||||
|
|
||||||
echo "Generating changelog from commits..."
|
echo "Generating changelog from commits..."
|
||||||
if [[ -n "$PREVIOUS_TAG" ]]; then
|
if [[ -n "$PREVIOUS_TAG" ]]; then
|
||||||
@@ -100,16 +98,18 @@ jobs:
|
|||||||
COMMITS="- Initial release"
|
COMMITS="- Initial release"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create new changelog entry — insert after the marker comment
|
# Create new changelog entry
|
||||||
NEW_ENTRY=$(printf '%s\n' \
|
{
|
||||||
"" \
|
head -n 8 CHANGELOG.md
|
||||||
"## [${{ steps.version.outputs.version }}] - $DATE" \
|
echo ""
|
||||||
"" \
|
echo "## [${{ steps.version.outputs.version }}] - $DATE"
|
||||||
"### Changed" \
|
echo ""
|
||||||
"" \
|
echo "### Changed"
|
||||||
"$COMMITS")
|
echo ""
|
||||||
|
echo "$COMMITS"
|
||||||
awk -v entry="$NEW_ENTRY" '/<!-- insert new changelog below this comment -->/ { print; print entry; next } {print}' CHANGELOG.md > CHANGELOG.md.tmp
|
echo ""
|
||||||
|
tail -n +9 CHANGELOG.md
|
||||||
|
} > CHANGELOG.md.tmp
|
||||||
mv CHANGELOG.md.tmp CHANGELOG.md
|
mv CHANGELOG.md.tmp CHANGELOG.md
|
||||||
|
|
||||||
echo "✅ Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
|
echo "✅ Updated CHANGELOG.md with commits since $PREVIOUS_TAG"
|
||||||
@@ -139,22 +139,6 @@ jobs:
|
|||||||
git push origin "${{ steps.version.outputs.tag }}"
|
git push origin "${{ steps.version.outputs.tag }}"
|
||||||
echo "Branch ${{ env.branch }} and tag ${{ steps.version.outputs.tag }} pushed"
|
echo "Branch ${{ env.branch }} and tag ${{ steps.version.outputs.tag }} pushed"
|
||||||
|
|
||||||
- name: Bump to dev version
|
|
||||||
id: dev_version
|
|
||||||
run: |
|
|
||||||
IFS='.' read -r MAJOR MINOR PATCH <<< "${{ steps.version.outputs.version }}"
|
|
||||||
NEXT_DEV="$MAJOR.$MINOR.$((PATCH + 1)).dev0"
|
|
||||||
echo "dev_version=$NEXT_DEV" >> $GITHUB_OUTPUT
|
|
||||||
sed -i "s/version = \".*\"/version = \"$NEXT_DEV\"/" pyproject.toml
|
|
||||||
git add pyproject.toml
|
|
||||||
if git diff --cached --quiet; then
|
|
||||||
echo "No dev version changes to commit"
|
|
||||||
else
|
|
||||||
git commit -m "chore: begin $NEXT_DEV development"
|
|
||||||
git push origin "${{ env.branch }}"
|
|
||||||
echo "Bumped to dev version $NEXT_DEV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Open pull request
|
- name: Open pull request
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||||
@@ -162,17 +146,16 @@ jobs:
|
|||||||
gh pr create \
|
gh pr create \
|
||||||
--base main \
|
--base main \
|
||||||
--head "${{ env.branch }}" \
|
--head "${{ env.branch }}" \
|
||||||
--title "chore: release ${{ steps.version.outputs.version }}, begin ${{ steps.dev_version.outputs.dev_version }} development" \
|
--title "chore: bump version to ${{ steps.version.outputs.version }}" \
|
||||||
--body "Automated release of ${{ steps.version.outputs.version }}.
|
--body "Automated version bump to ${{ steps.version.outputs.version }}.
|
||||||
|
|
||||||
This PR was created by the Release Trigger workflow. The git tag \`${{ steps.version.outputs.tag }}\` has already been pushed and the release artifacts are being built.
|
This PR was created by the Release Trigger workflow. The git tag \`${{ steps.version.outputs.tag }}\` has already been pushed and the release artifacts are being built.
|
||||||
|
|
||||||
Merging this PR will set \`main\` to \`${{ steps.dev_version.outputs.dev_version }}\` so that development installs are clearly marked as pre-release."
|
Merge this PR to record the version bump and changelog update on \`main\`."
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
|
echo "✅ Version bumped to ${{ steps.version.outputs.version }}"
|
||||||
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
|
echo "✅ Tag ${{ steps.version.outputs.tag }} created and pushed"
|
||||||
echo "✅ Dev version set to ${{ steps.dev_version.outputs.dev_version }}"
|
|
||||||
echo "✅ PR opened to merge version bump into main"
|
echo "✅ PR opened to merge version bump into main"
|
||||||
echo "🚀 Release workflow is building artifacts from the tag"
|
echo "🚀 Release workflow is building artifacts from the tag"
|
||||||
|
|||||||
62
.github/workflows/release.yml
vendored
62
.github/workflows/release.yml
vendored
@@ -27,63 +27,35 @@ jobs:
|
|||||||
- name: Check if release already exists
|
- name: Check if release already exists
|
||||||
id: check_release
|
id: check_release
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.tag }}"
|
chmod +x .github/workflows/scripts/check-release-exists.sh
|
||||||
if gh release view "$VERSION" >/dev/null 2>&1; then
|
.github/workflows/scripts/check-release-exists.sh ${{ steps.version.outputs.tag }}
|
||||||
echo "exists=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Release $VERSION already exists, skipping..."
|
|
||||||
else
|
|
||||||
echo "exists=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "Release $VERSION does not exist, proceeding..."
|
|
||||||
fi
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Generate release notes
|
- name: Create release package variants
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.tag }}"
|
chmod +x .github/workflows/scripts/create-release-packages.sh
|
||||||
VERSION_NO_V=${VERSION#v}
|
.github/workflows/scripts/create-release-packages.sh ${{ steps.version.outputs.tag }}
|
||||||
|
|
||||||
# Find previous tag
|
- name: Generate release notes
|
||||||
PREVIOUS_TAG=$(git tag -l 'v*' --sort=-version:refname | grep -v "^${VERSION}$" | head -n 1)
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
|
id: release_notes
|
||||||
|
run: |
|
||||||
|
chmod +x .github/workflows/scripts/generate-release-notes.sh
|
||||||
|
# Get the previous tag for changelog generation
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 ${{ steps.version.outputs.tag }}^ 2>/dev/null || echo "")
|
||||||
|
# Default to v0.0.0 if no previous tag is found (e.g., first release)
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
PREVIOUS_TAG=""
|
PREVIOUS_TAG="v0.0.0"
|
||||||
fi
|
fi
|
||||||
|
.github/workflows/scripts/generate-release-notes.sh ${{ steps.version.outputs.tag }} "$PREVIOUS_TAG"
|
||||||
# Get commits since previous tag
|
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
|
||||||
if [ "$COMMIT_COUNT" -gt 20 ]; then
|
|
||||||
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges HEAD~20..HEAD)
|
|
||||||
else
|
|
||||||
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges)
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
COMMITS=$(git log --oneline --pretty=format:"- %s" --no-merges "$PREVIOUS_TAG"..HEAD)
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > release_notes.md << NOTES_EOF
|
|
||||||
## Install
|
|
||||||
|
|
||||||
\`\`\`bash
|
|
||||||
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@${VERSION}
|
|
||||||
specify init my-project
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
NOTES_EOF
|
|
||||||
|
|
||||||
echo "## What's Changed" >> release_notes.md
|
|
||||||
echo "" >> release_notes.md
|
|
||||||
echo "$COMMITS" >> release_notes.md
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
if: steps.check_release.outputs.exists == 'false'
|
if: steps.check_release.outputs.exists == 'false'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.tag }}"
|
chmod +x .github/workflows/scripts/create-github-release.sh
|
||||||
VERSION_NO_V=${VERSION#v}
|
.github/workflows/scripts/create-github-release.sh ${{ steps.version.outputs.tag }}
|
||||||
gh release create "$VERSION" \
|
|
||||||
--title "Spec Kit - $VERSION_NO_V" \
|
|
||||||
--notes-file release_notes.md
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|||||||
21
.github/workflows/scripts/check-release-exists.sh
vendored
Normal file
21
.github/workflows/scripts/check-release-exists.sh
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# check-release-exists.sh
|
||||||
|
# Check if a GitHub release already exists for the given version
|
||||||
|
# Usage: check-release-exists.sh <version>
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
if gh release view "$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $VERSION already exists, skipping..."
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "Release $VERSION does not exist, proceeding..."
|
||||||
|
fi
|
||||||
58
.github/workflows/scripts/create-github-release.sh
vendored
Normal file
58
.github/workflows/scripts/create-github-release.sh
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# create-github-release.sh
|
||||||
|
# Create a GitHub release with all template zip files
|
||||||
|
# Usage: create-github-release.sh <version>
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
# Remove 'v' prefix from version for release title
|
||||||
|
VERSION_NO_V=${VERSION#v}
|
||||||
|
|
||||||
|
gh release create "$VERSION" \
|
||||||
|
.genreleases/spec-kit-template-copilot-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-copilot-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-claude-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-claude-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-gemini-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-gemini-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-cursor-agent-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-cursor-agent-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-opencode-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-opencode-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-qwen-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-qwen-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-windsurf-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-windsurf-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-codex-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-codex-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-kilocode-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-kilocode-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-auggie-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-auggie-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-roo-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-roo-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-codebuddy-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-codebuddy-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-qodercli-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-qodercli-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-amp-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-amp-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-shai-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-shai-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-kiro-cli-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-kiro-cli-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-agy-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-agy-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-bob-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-bob-ps-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-generic-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-generic-ps-"$VERSION".zip \
|
||||||
|
--title "Spec Kit Templates - $VERSION_NO_V" \
|
||||||
|
--notes-file release_notes.md
|
||||||
439
.github/workflows/scripts/create-release-packages.ps1
vendored
Normal file
439
.github/workflows/scripts/create-release-packages.ps1
vendored
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
#requires -Version 7.0
|
||||||
|
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
create-release-packages.ps1 (workflow-local)
|
||||||
|
Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||||
|
|
||||||
|
.PARAMETER Version
|
||||||
|
Version string with leading 'v' (e.g., v0.2.0)
|
||||||
|
|
||||||
|
.PARAMETER Agents
|
||||||
|
Comma or space separated subset of agents to build (default: all)
|
||||||
|
Valid agents: claude, gemini, copilot, cursor-agent, qwen, opencode, windsurf, codex, kilocode, auggie, roo, codebuddy, amp, kiro-cli, bob, qodercli, shai, agy, generic
|
||||||
|
|
||||||
|
.PARAMETER Scripts
|
||||||
|
Comma or space separated subset of script types to build (default: both)
|
||||||
|
Valid scripts: sh, ps
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\create-release-packages.ps1 -Version v0.2.0
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude,copilot -Scripts sh
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
.\create-release-packages.ps1 -Version v0.2.0 -Agents claude -Scripts ps
|
||||||
|
#>
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true, Position=0)]
|
||||||
|
[string]$Version,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Agents = "",
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$false)]
|
||||||
|
[string]$Scripts = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Validate version format
|
||||||
|
if ($Version -notmatch '^v\d+\.\d+\.\d+$') {
|
||||||
|
Write-Error "Version must look like v0.0.0"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Building release packages for $Version"
|
||||||
|
|
||||||
|
# Create and use .genreleases directory for all build artifacts
|
||||||
|
$GenReleasesDir = ".genreleases"
|
||||||
|
if (Test-Path $GenReleasesDir) {
|
||||||
|
Remove-Item -Path $GenReleasesDir -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $GenReleasesDir -Force | Out-Null
|
||||||
|
|
||||||
|
function Rewrite-Paths {
|
||||||
|
param([string]$Content)
|
||||||
|
|
||||||
|
$Content = $Content -replace '(/?)\bmemory/', '.specify/memory/'
|
||||||
|
$Content = $Content -replace '(/?)\bscripts/', '.specify/scripts/'
|
||||||
|
$Content = $Content -replace '(/?)\btemplates/', '.specify/templates/'
|
||||||
|
return $Content
|
||||||
|
}
|
||||||
|
|
||||||
|
function Generate-Commands {
|
||||||
|
param(
|
||||||
|
[string]$Agent,
|
||||||
|
[string]$Extension,
|
||||||
|
[string]$ArgFormat,
|
||||||
|
[string]$OutputDir,
|
||||||
|
[string]$ScriptVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||||
|
|
||||||
|
$templates = Get-ChildItem -Path "templates/commands/*.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($template in $templates) {
|
||||||
|
$name = [System.IO.Path]::GetFileNameWithoutExtension($template.Name)
|
||||||
|
|
||||||
|
# Read file content and normalize line endings
|
||||||
|
$fileContent = (Get-Content -Path $template.FullName -Raw) -replace "`r`n", "`n"
|
||||||
|
|
||||||
|
# Extract description from YAML frontmatter
|
||||||
|
$description = ""
|
||||||
|
if ($fileContent -match '(?m)^description:\s*(.+)$') {
|
||||||
|
$description = $matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract script command from YAML frontmatter
|
||||||
|
$scriptCommand = ""
|
||||||
|
if ($fileContent -match "(?m)^\s*${ScriptVariant}:\s*(.+)$") {
|
||||||
|
$scriptCommand = $matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($scriptCommand)) {
|
||||||
|
Write-Warning "No script command found for $ScriptVariant in $($template.Name)"
|
||||||
|
$scriptCommand = "(Missing script command for $ScriptVariant)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract agent_script command from YAML frontmatter if present
|
||||||
|
$agentScriptCommand = ""
|
||||||
|
if ($fileContent -match "(?ms)agent_scripts:.*?^\s*${ScriptVariant}:\s*(.+?)$") {
|
||||||
|
$agentScriptCommand = $matches[1].Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Replace {SCRIPT} placeholder with the script command
|
||||||
|
$body = $fileContent -replace '\{SCRIPT\}', $scriptCommand
|
||||||
|
|
||||||
|
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
||||||
|
if (-not [string]::IsNullOrEmpty($agentScriptCommand)) {
|
||||||
|
$body = $body -replace '\{AGENT_SCRIPT\}', $agentScriptCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the scripts: and agent_scripts: sections from frontmatter
|
||||||
|
$lines = $body -split "`n"
|
||||||
|
$outputLines = @()
|
||||||
|
$inFrontmatter = $false
|
||||||
|
$skipScripts = $false
|
||||||
|
$dashCount = 0
|
||||||
|
|
||||||
|
foreach ($line in $lines) {
|
||||||
|
if ($line -match '^---$') {
|
||||||
|
$outputLines += $line
|
||||||
|
$dashCount++
|
||||||
|
if ($dashCount -eq 1) {
|
||||||
|
$inFrontmatter = $true
|
||||||
|
} else {
|
||||||
|
$inFrontmatter = $false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($inFrontmatter) {
|
||||||
|
if ($line -match '^(scripts|agent_scripts):$') {
|
||||||
|
$skipScripts = $true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ($line -match '^[a-zA-Z].*:' -and $skipScripts) {
|
||||||
|
$skipScripts = $false
|
||||||
|
}
|
||||||
|
if ($skipScripts -and $line -match '^\s+') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$outputLines += $line
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = $outputLines -join "`n"
|
||||||
|
|
||||||
|
# Apply other substitutions
|
||||||
|
$body = $body -replace '\{ARGS\}', $ArgFormat
|
||||||
|
$body = $body -replace '__AGENT__', $Agent
|
||||||
|
$body = Rewrite-Paths -Content $body
|
||||||
|
|
||||||
|
# Generate output file based on extension
|
||||||
|
$outputFile = Join-Path $OutputDir "speckit.$name.$Extension"
|
||||||
|
|
||||||
|
switch ($Extension) {
|
||||||
|
'toml' {
|
||||||
|
$body = $body -replace '\\', '\\'
|
||||||
|
$output = "description = `"$description`"`n`nprompt = `"`"`"`n$body`n`"`"`""
|
||||||
|
Set-Content -Path $outputFile -Value $output -NoNewline
|
||||||
|
}
|
||||||
|
'md' {
|
||||||
|
Set-Content -Path $outputFile -Value $body -NoNewline
|
||||||
|
}
|
||||||
|
'agent.md' {
|
||||||
|
Set-Content -Path $outputFile -Value $body -NoNewline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Generate-CopilotPrompts {
|
||||||
|
param(
|
||||||
|
[string]$AgentsDir,
|
||||||
|
[string]$PromptsDir
|
||||||
|
)
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Path $PromptsDir -Force | Out-Null
|
||||||
|
|
||||||
|
$agentFiles = Get-ChildItem -Path "$AgentsDir/speckit.*.agent.md" -File -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
foreach ($agentFile in $agentFiles) {
|
||||||
|
$basename = $agentFile.Name -replace '\.agent\.md$', ''
|
||||||
|
$promptFile = Join-Path $PromptsDir "$basename.prompt.md"
|
||||||
|
|
||||||
|
$content = @"
|
||||||
|
---
|
||||||
|
agent: $basename
|
||||||
|
---
|
||||||
|
"@
|
||||||
|
Set-Content -Path $promptFile -Value $content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Build-Variant {
|
||||||
|
param(
|
||||||
|
[string]$Agent,
|
||||||
|
[string]$Script
|
||||||
|
)
|
||||||
|
|
||||||
|
$baseDir = Join-Path $GenReleasesDir "sdd-${Agent}-package-${Script}"
|
||||||
|
Write-Host "Building $Agent ($Script) package..."
|
||||||
|
New-Item -ItemType Directory -Path $baseDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy base structure but filter scripts by variant
|
||||||
|
$specDir = Join-Path $baseDir ".specify"
|
||||||
|
New-Item -ItemType Directory -Path $specDir -Force | Out-Null
|
||||||
|
|
||||||
|
# Copy memory directory
|
||||||
|
if (Test-Path "memory") {
|
||||||
|
Copy-Item -Path "memory" -Destination $specDir -Recurse -Force
|
||||||
|
Write-Host "Copied memory -> .specify"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only copy the relevant script variant directory
|
||||||
|
if (Test-Path "scripts") {
|
||||||
|
$scriptsDestDir = Join-Path $specDir "scripts"
|
||||||
|
New-Item -ItemType Directory -Path $scriptsDestDir -Force | Out-Null
|
||||||
|
|
||||||
|
switch ($Script) {
|
||||||
|
'sh' {
|
||||||
|
if (Test-Path "scripts/bash") {
|
||||||
|
Copy-Item -Path "scripts/bash" -Destination $scriptsDestDir -Recurse -Force
|
||||||
|
Write-Host "Copied scripts/bash -> .specify/scripts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'ps' {
|
||||||
|
if (Test-Path "scripts/powershell") {
|
||||||
|
Copy-Item -Path "scripts/powershell" -Destination $scriptsDestDir -Recurse -Force
|
||||||
|
Write-Host "Copied scripts/powershell -> .specify/scripts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy any script files that aren't in variant-specific directories
|
||||||
|
Get-ChildItem -Path "scripts" -File -ErrorAction SilentlyContinue | ForEach-Object {
|
||||||
|
Copy-Item -Path $_.FullName -Destination $scriptsDestDir -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy templates (excluding commands directory and vscode-settings.json)
|
||||||
|
if (Test-Path "templates") {
|
||||||
|
$templatesDestDir = Join-Path $specDir "templates"
|
||||||
|
New-Item -ItemType Directory -Path $templatesDestDir -Force | Out-Null
|
||||||
|
|
||||||
|
Get-ChildItem -Path "templates" -Recurse -File | Where-Object {
|
||||||
|
$_.FullName -notmatch 'templates[/\\]commands[/\\]' -and $_.Name -ne 'vscode-settings.json'
|
||||||
|
} | ForEach-Object {
|
||||||
|
$relativePath = $_.FullName.Substring((Resolve-Path "templates").Path.Length + 1)
|
||||||
|
$destFile = Join-Path $templatesDestDir $relativePath
|
||||||
|
$destFileDir = Split-Path $destFile -Parent
|
||||||
|
New-Item -ItemType Directory -Path $destFileDir -Force | Out-Null
|
||||||
|
Copy-Item -Path $_.FullName -Destination $destFile -Force
|
||||||
|
}
|
||||||
|
Write-Host "Copied templates -> .specify/templates"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate agent-specific command files
|
||||||
|
switch ($Agent) {
|
||||||
|
'claude' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".claude/commands"
|
||||||
|
Generate-Commands -Agent 'claude' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'gemini' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".gemini/commands"
|
||||||
|
Generate-Commands -Agent 'gemini' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
if (Test-Path "agent_templates/gemini/GEMINI.md") {
|
||||||
|
Copy-Item -Path "agent_templates/gemini/GEMINI.md" -Destination (Join-Path $baseDir "GEMINI.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'copilot' {
|
||||||
|
$agentsDir = Join-Path $baseDir ".github/agents"
|
||||||
|
Generate-Commands -Agent 'copilot' -Extension 'agent.md' -ArgFormat '$ARGUMENTS' -OutputDir $agentsDir -ScriptVariant $Script
|
||||||
|
|
||||||
|
# Generate companion prompt files
|
||||||
|
$promptsDir = Join-Path $baseDir ".github/prompts"
|
||||||
|
Generate-CopilotPrompts -AgentsDir $agentsDir -PromptsDir $promptsDir
|
||||||
|
|
||||||
|
# Create VS Code workspace settings
|
||||||
|
$vscodeDir = Join-Path $baseDir ".vscode"
|
||||||
|
New-Item -ItemType Directory -Path $vscodeDir -Force | Out-Null
|
||||||
|
if (Test-Path "templates/vscode-settings.json") {
|
||||||
|
Copy-Item -Path "templates/vscode-settings.json" -Destination (Join-Path $vscodeDir "settings.json")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'cursor-agent' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".cursor/commands"
|
||||||
|
Generate-Commands -Agent 'cursor-agent' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'qwen' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".qwen/commands"
|
||||||
|
Generate-Commands -Agent 'qwen' -Extension 'toml' -ArgFormat '{{args}}' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
if (Test-Path "agent_templates/qwen/QWEN.md") {
|
||||||
|
Copy-Item -Path "agent_templates/qwen/QWEN.md" -Destination (Join-Path $baseDir "QWEN.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'opencode' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".opencode/command"
|
||||||
|
Generate-Commands -Agent 'opencode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'windsurf' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".windsurf/workflows"
|
||||||
|
Generate-Commands -Agent 'windsurf' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'codex' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".codex/prompts"
|
||||||
|
Generate-Commands -Agent 'codex' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'kilocode' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".kilocode/workflows"
|
||||||
|
Generate-Commands -Agent 'kilocode' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'auggie' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".augment/commands"
|
||||||
|
Generate-Commands -Agent 'auggie' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'roo' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".roo/commands"
|
||||||
|
Generate-Commands -Agent 'roo' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'codebuddy' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".codebuddy/commands"
|
||||||
|
Generate-Commands -Agent 'codebuddy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'amp' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".agents/commands"
|
||||||
|
Generate-Commands -Agent 'amp' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'kiro-cli' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".kiro/prompts"
|
||||||
|
Generate-Commands -Agent 'kiro-cli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'bob' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".bob/commands"
|
||||||
|
Generate-Commands -Agent 'bob' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'qodercli' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".qoder/commands"
|
||||||
|
Generate-Commands -Agent 'qodercli' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'shai' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".shai/commands"
|
||||||
|
Generate-Commands -Agent 'shai' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'agy' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".agent/workflows"
|
||||||
|
Generate-Commands -Agent 'agy' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
'generic' {
|
||||||
|
$cmdDir = Join-Path $baseDir ".speckit/commands"
|
||||||
|
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
throw "Unsupported agent '$Agent'."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create zip archive
|
||||||
|
$zipFile = Join-Path $GenReleasesDir "spec-kit-template-${Agent}-${Script}-${Version}.zip"
|
||||||
|
Compress-Archive -Path "$baseDir/*" -DestinationPath $zipFile -Force
|
||||||
|
Write-Host "Created $zipFile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Define all agents and scripts
|
||||||
|
$AllAgents = @('claude', 'gemini', 'copilot', 'cursor-agent', 'qwen', 'opencode', 'windsurf', 'codex', 'kilocode', 'auggie', 'roo', 'codebuddy', 'amp', 'kiro-cli', 'bob', 'qodercli', 'shai', 'agy', 'generic')
|
||||||
|
$AllScripts = @('sh', 'ps')
|
||||||
|
|
||||||
|
function Normalize-List {
|
||||||
|
param([string]$Input)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($Input)) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Split by comma or space and remove duplicates while preserving order
|
||||||
|
$items = $Input -split '[,\s]+' | Where-Object { $_ } | Select-Object -Unique
|
||||||
|
return $items
|
||||||
|
}
|
||||||
|
|
||||||
|
function Validate-Subset {
|
||||||
|
param(
|
||||||
|
[string]$Type,
|
||||||
|
[string[]]$Allowed,
|
||||||
|
[string[]]$Items
|
||||||
|
)
|
||||||
|
|
||||||
|
$ok = $true
|
||||||
|
foreach ($item in $Items) {
|
||||||
|
if ($item -notin $Allowed) {
|
||||||
|
Write-Error "Unknown $Type '$item' (allowed: $($Allowed -join ', '))"
|
||||||
|
$ok = $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $ok
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine agent list
|
||||||
|
if (-not [string]::IsNullOrEmpty($Agents)) {
|
||||||
|
$AgentList = Normalize-List -Input $Agents
|
||||||
|
if (-not (Validate-Subset -Type 'agent' -Allowed $AllAgents -Items $AgentList)) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$AgentList = $AllAgents
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine script list
|
||||||
|
if (-not [string]::IsNullOrEmpty($Scripts)) {
|
||||||
|
$ScriptList = Normalize-List -Input $Scripts
|
||||||
|
if (-not (Validate-Subset -Type 'script' -Allowed $AllScripts -Items $ScriptList)) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$ScriptList = $AllScripts
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Agents: $($AgentList -join ', ')"
|
||||||
|
Write-Host "Scripts: $($ScriptList -join ', ')"
|
||||||
|
|
||||||
|
# Build all variants
|
||||||
|
foreach ($agent in $AgentList) {
|
||||||
|
foreach ($script in $ScriptList) {
|
||||||
|
Build-Variant -Agent $agent -Script $script
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`nArchives in ${GenReleasesDir}:"
|
||||||
|
Get-ChildItem -Path $GenReleasesDir -Filter "spec-kit-template-*-${Version}.zip" | ForEach-Object {
|
||||||
|
Write-Host " $($_.Name)"
|
||||||
|
}
|
||||||
279
.github/workflows/scripts/create-release-packages.sh
vendored
Executable file
279
.github/workflows/scripts/create-release-packages.sh
vendored
Executable file
@@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# create-release-packages.sh (workflow-local)
|
||||||
|
# Build Spec Kit template release archives for each supported AI assistant and script type.
|
||||||
|
# Usage: .github/workflows/scripts/create-release-packages.sh <version>
|
||||||
|
# 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 cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai kiro-cli agy bob qodercli generic (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
|
||||||
|
echo "Usage: $0 <version-with-v-prefix>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
NEW_VERSION="$1"
|
||||||
|
if [[ ! $NEW_VERSION =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "Version must look like v0.0.0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building release packages for $NEW_VERSION"
|
||||||
|
|
||||||
|
# Create and use .genreleases directory for all build artifacts
|
||||||
|
GENRELEASES_DIR=".genreleases"
|
||||||
|
mkdir -p "$GENRELEASES_DIR"
|
||||||
|
rm -rf "$GENRELEASES_DIR"/* || true
|
||||||
|
|
||||||
|
rewrite_paths() {
|
||||||
|
sed -E \
|
||||||
|
-e 's@(/?)memory/@.specify/memory/@g' \
|
||||||
|
-e 's@(/?)scripts/@.specify/scripts/@g' \
|
||||||
|
-e 's@(/?)templates/@.specify/templates/@g' \
|
||||||
|
-e 's@\.specify\.specify/@.specify/@g'
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_commands() {
|
||||||
|
local agent=$1 ext=$2 arg_format=$3 output_dir=$4 script_variant=$5
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
for template in templates/commands/*.md; do
|
||||||
|
[[ -f "$template" ]] || continue
|
||||||
|
local name description script_command agent_script_command body
|
||||||
|
name=$(basename "$template" .md)
|
||||||
|
|
||||||
|
# Normalize line endings
|
||||||
|
file_content=$(tr -d '\r' < "$template")
|
||||||
|
|
||||||
|
# Extract description and script command from YAML frontmatter
|
||||||
|
description=$(printf '%s\n' "$file_content" | awk '/^description:/ {sub(/^description:[[:space:]]*/, ""); print; exit}')
|
||||||
|
script_command=$(printf '%s\n' "$file_content" | awk -v sv="$script_variant" '/^[[:space:]]*'"$script_variant"':[[:space:]]*/ {sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, ""); print; exit}')
|
||||||
|
|
||||||
|
if [[ -z $script_command ]]; then
|
||||||
|
echo "Warning: no script command found for $script_variant in $template" >&2
|
||||||
|
script_command="(Missing script command for $script_variant)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract agent_script command from YAML frontmatter if present
|
||||||
|
agent_script_command=$(printf '%s\n' "$file_content" | awk '
|
||||||
|
/^agent_scripts:$/ { in_agent_scripts=1; next }
|
||||||
|
in_agent_scripts && /^[[:space:]]*'"$script_variant"':[[:space:]]*/ {
|
||||||
|
sub(/^[[:space:]]*'"$script_variant"':[[:space:]]*/, "")
|
||||||
|
print
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
in_agent_scripts && /^[a-zA-Z]/ { in_agent_scripts=0 }
|
||||||
|
')
|
||||||
|
|
||||||
|
# Replace {SCRIPT} placeholder with the script command
|
||||||
|
body=$(printf '%s\n' "$file_content" | sed "s|{SCRIPT}|${script_command}|g")
|
||||||
|
|
||||||
|
# Replace {AGENT_SCRIPT} placeholder with the agent script command if found
|
||||||
|
if [[ -n $agent_script_command ]]; then
|
||||||
|
body=$(printf '%s\n' "$body" | sed "s|{AGENT_SCRIPT}|${agent_script_command}|g")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove the scripts: and agent_scripts: sections from frontmatter while preserving YAML structure
|
||||||
|
body=$(printf '%s\n' "$body" | awk '
|
||||||
|
/^---$/ { print; if (++dash_count == 1) in_frontmatter=1; else in_frontmatter=0; next }
|
||||||
|
in_frontmatter && /^scripts:$/ { skip_scripts=1; next }
|
||||||
|
in_frontmatter && /^agent_scripts:$/ { skip_scripts=1; next }
|
||||||
|
in_frontmatter && /^[a-zA-Z].*:/ && skip_scripts { skip_scripts=0 }
|
||||||
|
in_frontmatter && skip_scripts && /^[[:space:]]/ { next }
|
||||||
|
{ print }
|
||||||
|
')
|
||||||
|
|
||||||
|
# Apply other substitutions
|
||||||
|
body=$(printf '%s\n' "$body" | sed "s/{ARGS}/$arg_format/g" | sed "s/__AGENT__/$agent/g" | rewrite_paths)
|
||||||
|
|
||||||
|
case $ext in
|
||||||
|
toml)
|
||||||
|
body=$(printf '%s\n' "$body" | sed 's/\\/\\\\/g')
|
||||||
|
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/speckit.$name.$ext" ;;
|
||||||
|
md)
|
||||||
|
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
||||||
|
agent.md)
|
||||||
|
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_copilot_prompts() {
|
||||||
|
local agents_dir=$1 prompts_dir=$2
|
||||||
|
mkdir -p "$prompts_dir"
|
||||||
|
|
||||||
|
# Generate a .prompt.md file for each .agent.md file
|
||||||
|
for agent_file in "$agents_dir"/speckit.*.agent.md; do
|
||||||
|
[[ -f "$agent_file" ]] || continue
|
||||||
|
|
||||||
|
local basename=$(basename "$agent_file" .agent.md)
|
||||||
|
local prompt_file="$prompts_dir/${basename}.prompt.md"
|
||||||
|
|
||||||
|
# Create prompt file with agent frontmatter
|
||||||
|
cat > "$prompt_file" <<EOF
|
||||||
|
---
|
||||||
|
agent: ${basename}
|
||||||
|
---
|
||||||
|
EOF
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
build_variant() {
|
||||||
|
local agent=$1 script=$2
|
||||||
|
local base_dir="$GENRELEASES_DIR/sdd-${agent}-package-${script}"
|
||||||
|
echo "Building $agent ($script) package..."
|
||||||
|
mkdir -p "$base_dir"
|
||||||
|
|
||||||
|
# Copy base structure but filter scripts by variant
|
||||||
|
SPEC_DIR="$base_dir/.specify"
|
||||||
|
mkdir -p "$SPEC_DIR"
|
||||||
|
|
||||||
|
[[ -d memory ]] && { cp -r memory "$SPEC_DIR/"; echo "Copied memory -> .specify"; }
|
||||||
|
|
||||||
|
# Only copy the relevant script variant directory
|
||||||
|
if [[ -d scripts ]]; then
|
||||||
|
mkdir -p "$SPEC_DIR/scripts"
|
||||||
|
case $script in
|
||||||
|
sh)
|
||||||
|
[[ -d scripts/bash ]] && { cp -r scripts/bash "$SPEC_DIR/scripts/"; echo "Copied scripts/bash -> .specify/scripts"; }
|
||||||
|
# Copy any script files that aren't in variant-specific directories
|
||||||
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
ps)
|
||||||
|
[[ -d scripts/powershell ]] && { cp -r scripts/powershell "$SPEC_DIR/scripts/"; echo "Copied scripts/powershell -> .specify/scripts"; }
|
||||||
|
# Copy any script files that aren't in variant-specific directories
|
||||||
|
find scripts -maxdepth 1 -type f -exec cp {} "$SPEC_DIR/scripts/" \; 2>/dev/null || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -not -name "vscode-settings.json" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
|
||||||
|
|
||||||
|
# NOTE: We substitute {ARGS} internally. Outward tokens differ intentionally:
|
||||||
|
# * Markdown/prompt (claude, copilot, cursor-agent, opencode): $ARGUMENTS
|
||||||
|
# * TOML (gemini, qwen): {{args}}
|
||||||
|
# This keeps formats readable without extra abstraction.
|
||||||
|
|
||||||
|
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/agents"
|
||||||
|
generate_commands copilot agent.md "\$ARGUMENTS" "$base_dir/.github/agents" "$script"
|
||||||
|
# Generate companion prompt files
|
||||||
|
generate_copilot_prompts "$base_dir/.github/agents" "$base_dir/.github/prompts"
|
||||||
|
# Create VS Code workspace settings
|
||||||
|
mkdir -p "$base_dir/.vscode"
|
||||||
|
[[ -f templates/vscode-settings.json ]] && cp templates/vscode-settings.json "$base_dir/.vscode/settings.json"
|
||||||
|
;;
|
||||||
|
cursor-agent)
|
||||||
|
mkdir -p "$base_dir/.cursor/commands"
|
||||||
|
generate_commands cursor-agent md "\$ARGUMENTS" "$base_dir/.cursor/commands" "$script" ;;
|
||||||
|
qwen)
|
||||||
|
mkdir -p "$base_dir/.qwen/commands"
|
||||||
|
generate_commands qwen toml "{{args}}" "$base_dir/.qwen/commands" "$script"
|
||||||
|
[[ -f agent_templates/qwen/QWEN.md ]] && cp agent_templates/qwen/QWEN.md "$base_dir/QWEN.md" ;;
|
||||||
|
opencode)
|
||||||
|
mkdir -p "$base_dir/.opencode/command"
|
||||||
|
generate_commands opencode md "\$ARGUMENTS" "$base_dir/.opencode/command" "$script" ;;
|
||||||
|
windsurf)
|
||||||
|
mkdir -p "$base_dir/.windsurf/workflows"
|
||||||
|
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
|
||||||
|
codex)
|
||||||
|
mkdir -p "$base_dir/.codex/prompts"
|
||||||
|
generate_commands codex md "\$ARGUMENTS" "$base_dir/.codex/prompts" "$script" ;;
|
||||||
|
kilocode)
|
||||||
|
mkdir -p "$base_dir/.kilocode/workflows"
|
||||||
|
generate_commands kilocode md "\$ARGUMENTS" "$base_dir/.kilocode/workflows" "$script" ;;
|
||||||
|
auggie)
|
||||||
|
mkdir -p "$base_dir/.augment/commands"
|
||||||
|
generate_commands auggie md "\$ARGUMENTS" "$base_dir/.augment/commands" "$script" ;;
|
||||||
|
roo)
|
||||||
|
mkdir -p "$base_dir/.roo/commands"
|
||||||
|
generate_commands roo md "\$ARGUMENTS" "$base_dir/.roo/commands" "$script" ;;
|
||||||
|
codebuddy)
|
||||||
|
mkdir -p "$base_dir/.codebuddy/commands"
|
||||||
|
generate_commands codebuddy md "\$ARGUMENTS" "$base_dir/.codebuddy/commands" "$script" ;;
|
||||||
|
qodercli)
|
||||||
|
mkdir -p "$base_dir/.qoder/commands"
|
||||||
|
generate_commands qodercli md "\$ARGUMENTS" "$base_dir/.qoder/commands" "$script" ;;
|
||||||
|
amp)
|
||||||
|
mkdir -p "$base_dir/.agents/commands"
|
||||||
|
generate_commands amp md "\$ARGUMENTS" "$base_dir/.agents/commands" "$script" ;;
|
||||||
|
shai)
|
||||||
|
mkdir -p "$base_dir/.shai/commands"
|
||||||
|
generate_commands shai md "\$ARGUMENTS" "$base_dir/.shai/commands" "$script" ;;
|
||||||
|
kiro-cli)
|
||||||
|
mkdir -p "$base_dir/.kiro/prompts"
|
||||||
|
generate_commands kiro-cli md "\$ARGUMENTS" "$base_dir/.kiro/prompts" "$script" ;;
|
||||||
|
agy)
|
||||||
|
mkdir -p "$base_dir/.agent/workflows"
|
||||||
|
generate_commands agy md "\$ARGUMENTS" "$base_dir/.agent/workflows" "$script" ;;
|
||||||
|
bob)
|
||||||
|
mkdir -p "$base_dir/.bob/commands"
|
||||||
|
generate_commands bob md "\$ARGUMENTS" "$base_dir/.bob/commands" "$script" ;;
|
||||||
|
generic)
|
||||||
|
mkdir -p "$base_dir/.speckit/commands"
|
||||||
|
generate_commands generic md "\$ARGUMENTS" "$base_dir/.speckit/commands" "$script" ;;
|
||||||
|
esac
|
||||||
|
( cd "$base_dir" && zip -r "../spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip" . )
|
||||||
|
echo "Created $GENRELEASES_DIR/spec-kit-template-${agent}-${script}-${NEW_VERSION}.zip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine agent list
|
||||||
|
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf codex kilocode auggie roo codebuddy amp shai kiro-cli agy bob qodercli generic)
|
||||||
|
ALL_SCRIPTS=(sh ps)
|
||||||
|
|
||||||
|
norm_list() {
|
||||||
|
# convert comma+space separated -> line separated unique while preserving order of first occurrence
|
||||||
|
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?"\n":"") $i);out=1}}}END{printf("\n")}'
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_subset() {
|
||||||
|
local type=$1; shift; local -n allowed=$1; shift; local items=("$@")
|
||||||
|
local invalid=0
|
||||||
|
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
|
||||||
|
invalid=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
return $invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -n ${AGENTS:-} ]]; then
|
||||||
|
mapfile -t 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
|
||||||
|
mapfile -t 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 in $GENRELEASES_DIR:"
|
||||||
|
ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip
|
||||||
40
.github/workflows/scripts/generate-release-notes.sh
vendored
Normal file
40
.github/workflows/scripts/generate-release-notes.sh
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# generate-release-notes.sh
|
||||||
|
# Generate release notes from git history
|
||||||
|
# Usage: generate-release-notes.sh <new_version> <last_tag>
|
||||||
|
|
||||||
|
if [[ $# -ne 2 ]]; then
|
||||||
|
echo "Usage: $0 <new_version> <last_tag>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NEW_VERSION="$1"
|
||||||
|
LAST_TAG="$2"
|
||||||
|
|
||||||
|
# Get commits since last 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
|
||||||
|
|
||||||
|
# Create release notes
|
||||||
|
cat > release_notes.md << EOF
|
||||||
|
This is the latest set of releases that you can use with your agent of choice. We recommend using the Specify CLI to scaffold your projects, however you can download these independently and manage them yourself.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
$COMMITS
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Generated release notes:"
|
||||||
|
cat release_notes.md
|
||||||
24
.github/workflows/scripts/get-next-version.sh
vendored
Normal file
24
.github/workflows/scripts/get-next-version.sh
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# get-next-version.sh
|
||||||
|
# Calculate the next version based on the latest git tag and output GitHub Actions variables
|
||||||
|
# Usage: get-next-version.sh
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Extract version number and increment
|
||||||
|
VERSION=$(echo $LATEST_TAG | sed 's/v//')
|
||||||
|
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
|
||||||
|
MAJOR=${VERSION_PARTS[0]:-0}
|
||||||
|
MINOR=${VERSION_PARTS[1]:-0}
|
||||||
|
PATCH=${VERSION_PARTS[2]:-0}
|
||||||
|
|
||||||
|
# Increment patch version
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
NEW_VERSION="v$MAJOR.$MINOR.$PATCH"
|
||||||
|
|
||||||
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "New version will be: $NEW_VERSION"
|
||||||
161
.github/workflows/scripts/simulate-release.sh
vendored
Executable file
161
.github/workflows/scripts/simulate-release.sh
vendored
Executable file
@@ -0,0 +1,161 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# simulate-release.sh
|
||||||
|
# Simulate the release process locally without pushing to GitHub
|
||||||
|
# Usage: simulate-release.sh [version]
|
||||||
|
# If version is omitted, auto-increments patch version
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
echo -e "${BLUE}🧪 Simulating Release Process Locally${NC}"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 1: Determine version
|
||||||
|
if [[ -n "${1:-}" ]]; then
|
||||||
|
VERSION="${1#v}"
|
||||||
|
TAG="v$VERSION"
|
||||||
|
echo -e "${GREEN}📝 Using manual version: $VERSION${NC}"
|
||||||
|
else
|
||||||
|
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
|
||||||
|
echo -e "${BLUE}Latest tag: $LATEST_TAG${NC}"
|
||||||
|
|
||||||
|
VERSION=$(echo $LATEST_TAG | sed 's/v//')
|
||||||
|
IFS='.' read -ra VERSION_PARTS <<< "$VERSION"
|
||||||
|
MAJOR=${VERSION_PARTS[0]:-0}
|
||||||
|
MINOR=${VERSION_PARTS[1]:-0}
|
||||||
|
PATCH=${VERSION_PARTS[2]:-0}
|
||||||
|
|
||||||
|
PATCH=$((PATCH + 1))
|
||||||
|
VERSION="$MAJOR.$MINOR.$PATCH"
|
||||||
|
TAG="v$VERSION"
|
||||||
|
echo -e "${GREEN}📝 Auto-incremented to: $VERSION${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: Check if tag exists
|
||||||
|
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}❌ Error: Tag $TAG already exists!${NC}"
|
||||||
|
echo " Please use a different version or delete the tag first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}✓ Tag $TAG is available${NC}"
|
||||||
|
|
||||||
|
# Step 3: Backup current state
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}💾 Creating backup of current state...${NC}"
|
||||||
|
BACKUP_DIR=$(mktemp -d)
|
||||||
|
cp pyproject.toml "$BACKUP_DIR/pyproject.toml.bak"
|
||||||
|
cp CHANGELOG.md "$BACKUP_DIR/CHANGELOG.md.bak"
|
||||||
|
echo -e "${GREEN}✓ Backup created at: $BACKUP_DIR${NC}"
|
||||||
|
|
||||||
|
# Step 4: Update pyproject.toml
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📝 Updating pyproject.toml...${NC}"
|
||||||
|
sed -i.tmp "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml
|
||||||
|
rm -f pyproject.toml.tmp
|
||||||
|
echo -e "${GREEN}✓ Updated pyproject.toml to version $VERSION${NC}"
|
||||||
|
|
||||||
|
# Step 5: Update CHANGELOG.md
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📝 Updating CHANGELOG.md...${NC}"
|
||||||
|
DATE=$(date +%Y-%m-%d)
|
||||||
|
|
||||||
|
# Get the previous tag to compare commits
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$PREVIOUS_TAG" ]]; then
|
||||||
|
echo " Generating changelog from commits since $PREVIOUS_TAG"
|
||||||
|
# Get commits since last tag, format as bullet points
|
||||||
|
COMMITS=$(git log --oneline "$PREVIOUS_TAG"..HEAD --no-merges --pretty=format:"- %s" 2>/dev/null || echo "- Initial release")
|
||||||
|
else
|
||||||
|
echo " No previous tag found - this is the first release"
|
||||||
|
COMMITS="- Initial release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create temp file with new entry
|
||||||
|
{
|
||||||
|
head -n 8 CHANGELOG.md
|
||||||
|
echo ""
|
||||||
|
echo "## [$VERSION] - $DATE"
|
||||||
|
echo ""
|
||||||
|
echo "### Changed"
|
||||||
|
echo ""
|
||||||
|
echo "$COMMITS"
|
||||||
|
echo ""
|
||||||
|
tail -n +9 CHANGELOG.md
|
||||||
|
} > CHANGELOG.md.tmp
|
||||||
|
mv CHANGELOG.md.tmp CHANGELOG.md
|
||||||
|
echo -e "${GREEN}✓ Updated CHANGELOG.md with commits since $PREVIOUS_TAG${NC}"
|
||||||
|
|
||||||
|
# Step 6: Show what would be committed
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📋 Changes that would be committed:${NC}"
|
||||||
|
git diff pyproject.toml CHANGELOG.md
|
||||||
|
|
||||||
|
# Step 7: Create temporary tag (no push)
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}🏷️ Creating temporary local tag...${NC}"
|
||||||
|
git tag -a "$TAG" -m "Simulated release $TAG" 2>/dev/null || true
|
||||||
|
echo -e "${GREEN}✓ Tag $TAG created locally${NC}"
|
||||||
|
|
||||||
|
# Step 8: Simulate release artifact creation
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📦 Simulating release package creation...${NC}"
|
||||||
|
echo " (High-level simulation only; packaging script is not executed)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if script exists and is executable
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
if [[ -x "$SCRIPT_DIR/create-release-packages.sh" ]]; then
|
||||||
|
echo -e "${BLUE}In a real release, the following command would be run to create packages:${NC}"
|
||||||
|
echo " $SCRIPT_DIR/create-release-packages.sh \"$TAG\""
|
||||||
|
echo ""
|
||||||
|
echo "This simulation does not enumerate individual package files to avoid"
|
||||||
|
echo "drifting from the actual behavior of create-release-packages.sh."
|
||||||
|
else
|
||||||
|
echo -e "${RED}⚠️ create-release-packages.sh not found or not executable${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 9: Simulate release notes generation
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}📄 Simulating release notes generation...${NC}"
|
||||||
|
echo ""
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 $TAG^ 2>/dev/null || echo "")
|
||||||
|
if [[ -n "$PREVIOUS_TAG" ]]; then
|
||||||
|
echo -e "${BLUE}Changes since $PREVIOUS_TAG:${NC}"
|
||||||
|
git log --oneline "$PREVIOUS_TAG".."$TAG" | head -n 10
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
echo -e "${BLUE}No previous tag found - this would be the first release${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 10: Summary
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}🎉 Simulation Complete!${NC}"
|
||||||
|
echo "======================================"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Summary:${NC}"
|
||||||
|
echo " Version: $VERSION"
|
||||||
|
echo " Tag: $TAG"
|
||||||
|
echo " Backup: $BACKUP_DIR"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}⚠️ SIMULATION ONLY - NO CHANGES PUSHED${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Next steps:${NC}"
|
||||||
|
echo " 1. Review the changes above"
|
||||||
|
echo " 2. To keep changes: git add pyproject.toml CHANGELOG.md && git commit"
|
||||||
|
echo " 3. To discard changes: git checkout pyproject.toml CHANGELOG.md && git tag -d $TAG"
|
||||||
|
echo " 4. To restore from backup: cp $BACKUP_DIR/* ."
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}To run the actual release:${NC}"
|
||||||
|
echo " Go to: https://github.com/github/spec-kit/actions/workflows/release-trigger.yml"
|
||||||
|
echo " Click 'Run workflow' and enter version: $VERSION"
|
||||||
|
echo ""
|
||||||
23
.github/workflows/scripts/update-version.sh
vendored
Normal file
23
.github/workflows/scripts/update-version.sh
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# update-version.sh
|
||||||
|
# Update version in pyproject.toml (for release artifacts only)
|
||||||
|
# Usage: update-version.sh <version>
|
||||||
|
|
||||||
|
if [[ $# -ne 1 ]]; then
|
||||||
|
echo "Usage: $0 <version>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="$1"
|
||||||
|
|
||||||
|
# Remove 'v' prefix for Python versioning
|
||||||
|
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)"
|
||||||
|
else
|
||||||
|
echo "Warning: pyproject.toml not found, skipping version update"
|
||||||
|
fi
|
||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -39,4 +39,4 @@ jobs:
|
|||||||
any-of-labels: ''
|
any-of-labels: ''
|
||||||
|
|
||||||
# Operations per run (helps avoid rate limits)
|
# Operations per run (helps avoid rate limits)
|
||||||
operations-per-run: 250
|
operations-per-run: 100
|
||||||
|
|||||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
|
|||||||
511
AGENTS.md
511
AGENTS.md
@@ -10,278 +10,272 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Integration Architecture
|
## General practices
|
||||||
|
|
||||||
Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations/<key>/`. The package exposes a single class that declares all metadata, inherits setup/teardown logic from a base class, and registers itself in the global `INTEGRATION_REGISTRY` at import time.
|
- Any changes to `__init__.py` for the Specify CLI require a version rev in `pyproject.toml` and addition of entries to `CHANGELOG.md`.
|
||||||
|
|
||||||
```
|
## Adding New Agent Support
|
||||||
src/specify_cli/integrations/
|
|
||||||
├── __init__.py # INTEGRATION_REGISTRY + _register_builtins()
|
|
||||||
├── base.py # IntegrationBase, MarkdownIntegration, TomlIntegration, SkillsIntegration
|
|
||||||
├── manifest.py # IntegrationManifest (file tracking)
|
|
||||||
├── claude/ # Example: SkillsIntegration subclass
|
|
||||||
│ ├── __init__.py # ClaudeIntegration class
|
|
||||||
│ └── scripts/ # Thin wrapper scripts
|
|
||||||
│ ├── update-context.sh
|
|
||||||
│ └── update-context.ps1
|
|
||||||
├── gemini/ # Example: TomlIntegration subclass
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ └── scripts/
|
|
||||||
├── windsurf/ # Example: MarkdownIntegration subclass
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ └── scripts/
|
|
||||||
├── copilot/ # Example: IntegrationBase subclass (custom setup)
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ └── scripts/
|
|
||||||
└── ... # One subpackage per supported agent
|
|
||||||
```
|
|
||||||
|
|
||||||
The registry is the **single source of truth**. Supported agents, their directories, formats, and capabilities are all derived from the integration classes — no separate tables or config dicts to maintain.
|
This section explains how to add support for new AI agents/assistants to the Specify CLI. Use this guide as a reference when integrating new AI tools into the Spec-Driven Development workflow.
|
||||||
|
|
||||||
---
|
### Overview
|
||||||
|
|
||||||
## Adding a New Integration
|
Specify supports multiple AI agents by generating agent-specific command files and directory structures when initializing projects. Each agent has its own conventions for:
|
||||||
|
|
||||||
### 1. Choose a base class
|
- **Command file formats** (Markdown, TOML, etc.)
|
||||||
|
- **Directory structures** (`.claude/commands/`, `.windsurf/workflows/`, etc.)
|
||||||
|
- **Command invocation patterns** (slash commands, CLI tools, etc.)
|
||||||
|
- **Argument passing conventions** (`$ARGUMENTS`, `{{args}}`, etc.)
|
||||||
|
|
||||||
| Your agent needs… | Subclass |
|
### Current Supported Agents
|
||||||
|---|---|
|
|
||||||
| Standard markdown commands (`.md`) | `MarkdownIntegration` |
|
|
||||||
| TOML-format commands (`.toml`) | `TomlIntegration` |
|
|
||||||
| Skill directories (`speckit-<name>/SKILL.md`) | `SkillsIntegration` |
|
|
||||||
| Fully custom output (companion files, settings merge, etc.) | `IntegrationBase` directly |
|
|
||||||
|
|
||||||
Most agents only need `MarkdownIntegration` — a minimal subclass with zero method overrides.
|
| Agent | Directory | Format | CLI Tool | Description |
|
||||||
|
| -------------------------- | ---------------------- | -------- | --------------- | --------------------------- |
|
||||||
|
| **Claude Code** | `.claude/commands/` | Markdown | `claude` | Anthropic's Claude Code CLI |
|
||||||
|
| **Gemini CLI** | `.gemini/commands/` | TOML | `gemini` | Google's Gemini CLI |
|
||||||
|
| **GitHub Copilot** | `.github/agents/` | Markdown | N/A (IDE-based) | GitHub Copilot in VS Code |
|
||||||
|
| **Cursor** | `.cursor/commands/` | Markdown | `cursor-agent` | Cursor CLI |
|
||||||
|
| **Qwen Code** | `.qwen/commands/` | TOML | `qwen` | Alibaba's Qwen Code CLI |
|
||||||
|
| **opencode** | `.opencode/command/` | Markdown | `opencode` | opencode CLI |
|
||||||
|
| **Codex CLI** | `.codex/commands/` | Markdown | `codex` | Codex CLI |
|
||||||
|
| **Windsurf** | `.windsurf/workflows/` | Markdown | N/A (IDE-based) | Windsurf IDE workflows |
|
||||||
|
| **Kilo Code** | `.kilocode/rules/` | Markdown | N/A (IDE-based) | Kilo Code IDE |
|
||||||
|
| **Auggie CLI** | `.augment/rules/` | Markdown | `auggie` | Auggie CLI |
|
||||||
|
| **Roo Code** | `.roo/rules/` | Markdown | N/A (IDE-based) | Roo Code IDE |
|
||||||
|
| **CodeBuddy CLI** | `.codebuddy/commands/` | Markdown | `codebuddy` | CodeBuddy CLI |
|
||||||
|
| **Qoder CLI** | `.qoder/commands/` | Markdown | `qodercli` | Qoder CLI |
|
||||||
|
| **Kiro CLI** | `.kiro/prompts/` | Markdown | `kiro-cli` | Kiro CLI |
|
||||||
|
| **Amp** | `.agents/commands/` | Markdown | `amp` | Amp CLI |
|
||||||
|
| **SHAI** | `.shai/commands/` | Markdown | `shai` | SHAI CLI |
|
||||||
|
| **IBM Bob** | `.bob/commands/` | Markdown | N/A (IDE-based) | IBM Bob IDE |
|
||||||
|
| **Generic** | User-specified via `--ai-commands-dir` | Markdown | N/A | Bring your own agent |
|
||||||
|
|
||||||
### 2. Create the subpackage
|
### Step-by-Step Integration Guide
|
||||||
|
|
||||||
Create `src/specify_cli/integrations/<key>/__init__.py`. The `key` **must match the actual CLI tool name** (the executable users install and run). Use a Python-safe directory name if the key contains hyphens (e.g., `kiro_cli/` for key `"kiro-cli"`).
|
Follow these steps to add a new agent (using a hypothetical new agent as an example):
|
||||||
|
|
||||||
**Minimal example — Markdown agent (Windsurf):**
|
#### 1. Add to AGENT_CONFIG
|
||||||
|
|
||||||
|
**IMPORTANT**: Use the actual CLI tool name as the key, not a shortened version.
|
||||||
|
|
||||||
|
Add the new agent to the `AGENT_CONFIG` dictionary in `src/specify_cli/__init__.py`. This is the **single source of truth** for all agent metadata:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
"""Windsurf IDE integration."""
|
AGENT_CONFIG = {
|
||||||
|
# ... existing agents ...
|
||||||
from ..base import MarkdownIntegration
|
"new-agent-cli": { # Use the ACTUAL CLI tool name (what users type in terminal)
|
||||||
|
"name": "New Agent Display Name",
|
||||||
|
"folder": ".newagent/", # Directory for agent files
|
||||||
class WindsurfIntegration(MarkdownIntegration):
|
"commands_subdir": "commands", # Subdirectory name for command files (default: "commands")
|
||||||
key = "windsurf"
|
"install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based)
|
||||||
config = {
|
"requires_cli": True, # True if CLI tool required, False for IDE-based agents
|
||||||
"name": "Windsurf",
|
},
|
||||||
"folder": ".windsurf/",
|
}
|
||||||
"commands_subdir": "workflows",
|
|
||||||
"install_url": None,
|
|
||||||
"requires_cli": False,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".windsurf/workflows",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md",
|
|
||||||
}
|
|
||||||
context_file = ".windsurf/rules/specify-rules.md"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**TOML agent (Gemini):**
|
**Key Design Principle**: The dictionary key should match the actual executable name that users install. For example:
|
||||||
|
|
||||||
|
- ✅ Use `"cursor-agent"` because the CLI tool is literally called `cursor-agent`
|
||||||
|
- ❌ Don't use `"cursor"` as a shortcut if the tool is `cursor-agent`
|
||||||
|
|
||||||
|
This eliminates the need for special-case mappings throughout the codebase.
|
||||||
|
|
||||||
|
**Field Explanations**:
|
||||||
|
|
||||||
|
- `name`: Human-readable display name shown to users
|
||||||
|
- `folder`: Directory where agent-specific files are stored (relative to project root)
|
||||||
|
- `commands_subdir`: Subdirectory name within the agent folder where command/prompt files are stored (default: `"commands"`)
|
||||||
|
- Most agents use `"commands"` (e.g., `.claude/commands/`)
|
||||||
|
- Some agents use alternative names: `"agents"` (copilot), `"workflows"` (windsurf, kilocode, agy), `"prompts"` (codex, kiro-cli), `"command"` (opencode - singular)
|
||||||
|
- This field enables `--ai-skills` to locate command templates correctly for skill generation
|
||||||
|
- `install_url`: Installation documentation URL (set to `None` for IDE-based agents)
|
||||||
|
- `requires_cli`: Whether the agent requires a CLI tool check during initialization
|
||||||
|
|
||||||
|
#### 2. Update CLI Help Text
|
||||||
|
|
||||||
|
Update the `--ai` parameter help text in the `init()` command to include the new agent:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
"""Gemini CLI integration."""
|
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, new-agent-cli, or kiro-cli"),
|
||||||
|
|
||||||
from ..base import TomlIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class GeminiIntegration(TomlIntegration):
|
|
||||||
key = "gemini"
|
|
||||||
config = {
|
|
||||||
"name": "Gemini CLI",
|
|
||||||
"folder": ".gemini/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": "https://github.com/google-gemini/gemini-cli",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".gemini/commands",
|
|
||||||
"format": "toml",
|
|
||||||
"args": "{{args}}",
|
|
||||||
"extension": ".toml",
|
|
||||||
}
|
|
||||||
context_file = "GEMINI.md"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Skills agent (Codex):**
|
Also update any function docstrings, examples, and error messages that list available agents.
|
||||||
|
|
||||||
```python
|
#### 3. Update README Documentation
|
||||||
"""Codex CLI integration — skills-based agent."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
Update the **Supported AI Agents** section in `README.md` to include the new agent:
|
||||||
|
|
||||||
from ..base import IntegrationOption, SkillsIntegration
|
- Add the new agent to the table with appropriate support level (Full/Partial)
|
||||||
|
- Include the agent's official website link
|
||||||
|
- Add any relevant notes about the agent's implementation
|
||||||
|
- Ensure the table formatting remains aligned and consistent
|
||||||
|
|
||||||
|
#### 4. Update Release Package Script
|
||||||
|
|
||||||
class CodexIntegration(SkillsIntegration):
|
Modify `.github/workflows/scripts/create-release-packages.sh`:
|
||||||
key = "codex"
|
|
||||||
config = {
|
|
||||||
"name": "Codex CLI",
|
|
||||||
"folder": ".agents/",
|
|
||||||
"commands_subdir": "skills",
|
|
||||||
"install_url": "https://github.com/openai/codex",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".agents/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md",
|
|
||||||
}
|
|
||||||
context_file = "AGENTS.md"
|
|
||||||
|
|
||||||
@classmethod
|
##### Add to ALL_AGENTS array
|
||||||
def options(cls) -> list[IntegrationOption]:
|
|
||||||
return [
|
|
||||||
IntegrationOption(
|
|
||||||
"--skills",
|
|
||||||
is_flag=True,
|
|
||||||
default=True,
|
|
||||||
help="Install as agent skills (default for Codex)",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Required fields
|
|
||||||
|
|
||||||
| Field | Location | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `key` | Class attribute | Unique identifier; must match the CLI executable name |
|
|
||||||
| `config` | Class attribute (dict) | Agent metadata: `name`, `folder`, `commands_subdir`, `install_url`, `requires_cli` |
|
|
||||||
| `registrar_config` | Class attribute (dict) | Command output config: `dir`, `format`, `args` placeholder, file `extension` |
|
|
||||||
| `context_file` | Class attribute (str or None) | Path to agent context/instructions file (e.g., `"CLAUDE.md"`, `".github/copilot-instructions.md"`) |
|
|
||||||
|
|
||||||
**Key design rule:** `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings.
|
|
||||||
|
|
||||||
### 3. Register it
|
|
||||||
|
|
||||||
In `src/specify_cli/integrations/__init__.py`, add one import and one `_register()` call inside `_register_builtins()`. Both lists are alphabetical:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def _register_builtins() -> None:
|
|
||||||
# -- Imports (alphabetical) -------------------------------------------
|
|
||||||
from .claude import ClaudeIntegration
|
|
||||||
# ...
|
|
||||||
from .newagent import NewAgentIntegration # ← add import
|
|
||||||
# ...
|
|
||||||
|
|
||||||
# -- Registration (alphabetical) --------------------------------------
|
|
||||||
_register(ClaudeIntegration())
|
|
||||||
# ...
|
|
||||||
_register(NewAgentIntegration()) # ← add registration
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Add scripts
|
|
||||||
|
|
||||||
Create two thin wrapper scripts in `src/specify_cli/integrations/<key>/scripts/` that delegate to the shared context-update scripts. Each is ~25 lines of boilerplate.
|
|
||||||
|
|
||||||
**`update-context.sh`:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/usr/bin/env bash
|
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf kiro-cli)
|
||||||
# update-context.sh — <Agent Name> integration: create/update <context_file>
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" <key>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**`update-context.ps1`:**
|
##### Add case statement for directory structure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
case $agent in
|
||||||
|
# ... existing cases ...
|
||||||
|
windsurf)
|
||||||
|
mkdir -p "$base_dir/.windsurf/workflows"
|
||||||
|
generate_commands windsurf md "\$ARGUMENTS" "$base_dir/.windsurf/workflows" "$script" ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Update GitHub Release Script
|
||||||
|
|
||||||
|
Modify `.github/workflows/scripts/create-github-release.sh` to include the new agent's packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh release create "$VERSION" \
|
||||||
|
# ... existing packages ...
|
||||||
|
.genreleases/spec-kit-template-windsurf-sh-"$VERSION".zip \
|
||||||
|
.genreleases/spec-kit-template-windsurf-ps-"$VERSION".zip \
|
||||||
|
# Add new agent packages here
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Update Agent Context Scripts
|
||||||
|
|
||||||
|
##### Bash script (`scripts/bash/update-agent-context.sh`)
|
||||||
|
|
||||||
|
Add file variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to case statement:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
case "$AGENT_TYPE" in
|
||||||
|
# ... existing cases ...
|
||||||
|
windsurf) update_agent_file "$WINDSURF_FILE" "Windsurf" ;;
|
||||||
|
"")
|
||||||
|
# ... existing checks ...
|
||||||
|
[ -f "$WINDSURF_FILE" ] && update_agent_file "$WINDSURF_FILE" "Windsurf";
|
||||||
|
# Update default creation condition
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
##### PowerShell script (`scripts/powershell/update-agent-context.ps1`)
|
||||||
|
|
||||||
|
Add file variable:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# update-context.ps1 — <Agent Name> integration: create/update <context_file>
|
$windsurfFile = Join-Path $repoRoot '.windsurf/rules/specify-rules.md'
|
||||||
$ErrorActionPreference = 'Stop'
|
```
|
||||||
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
Add to switch statement:
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
```powershell
|
||||||
$repoRoot = $scriptDir
|
switch ($AgentType) {
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
# ... existing cases ...
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
'windsurf' { Update-AgentFile $windsurfFile 'Windsurf' }
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
'' {
|
||||||
|
foreach ($pair in @(
|
||||||
|
# ... existing pairs ...
|
||||||
|
@{file=$windsurfFile; name='Windsurf'}
|
||||||
|
)) {
|
||||||
|
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
|
||||||
|
}
|
||||||
|
# Update default creation condition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Update CLI Tool Checks (Optional)
|
||||||
|
|
||||||
|
For agents that require CLI tools, add checks in the `check()` command and agent validation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In check() command
|
||||||
|
tracker.add("windsurf", "Windsurf IDE (optional)")
|
||||||
|
windsurf_ok = check_tool_for_tracker("windsurf", "https://windsurf.com/", tracker)
|
||||||
|
|
||||||
|
# In init validation (only if CLI tool required)
|
||||||
|
elif selected_ai == "windsurf":
|
||||||
|
if not check_tool("windsurf", "Install from: https://windsurf.com/"):
|
||||||
|
console.print("[red]Error:[/red] Windsurf CLI is required for Windsurf projects")
|
||||||
|
agent_tool_missing = True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: CLI tool checks are now handled automatically based on the `requires_cli` field in AGENT_CONFIG. No additional code changes needed in the `check()` or `init()` commands - they automatically loop through AGENT_CONFIG and check tools as needed.
|
||||||
|
|
||||||
|
## Important Design Decisions
|
||||||
|
|
||||||
|
### Using Actual CLI Tool Names as Keys
|
||||||
|
|
||||||
|
**CRITICAL**: When adding a new agent to AGENT_CONFIG, always use the **actual executable name** as the dictionary key, not a shortened or convenient version.
|
||||||
|
|
||||||
|
**Why this matters:**
|
||||||
|
|
||||||
|
- The `check_tool()` function uses `shutil.which(tool)` to find executables in the system PATH
|
||||||
|
- If the key doesn't match the actual CLI tool name, you'll need special-case mappings throughout the codebase
|
||||||
|
- This creates unnecessary complexity and maintenance burden
|
||||||
|
|
||||||
|
**Example - The Cursor Lesson:**
|
||||||
|
|
||||||
|
❌ **Wrong approach** (requires special-case mapping):
|
||||||
|
|
||||||
|
```python
|
||||||
|
AGENT_CONFIG = {
|
||||||
|
"cursor": { # Shorthand that doesn't match the actual tool
|
||||||
|
"name": "Cursor",
|
||||||
|
# ...
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType <key>
|
# Then you need special cases everywhere:
|
||||||
|
cli_tool = agent_key
|
||||||
|
if agent_key == "cursor":
|
||||||
|
cli_tool = "cursor-agent" # Map to the real tool name
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `<key>` with your integration key and `<Agent Name>` / `<context_file>` with the appropriate values.
|
✅ **Correct approach** (no mapping needed):
|
||||||
|
|
||||||
You must also add the agent to the shared context-update scripts so the shared dispatcher recognises the new key:
|
```python
|
||||||
|
AGENT_CONFIG = {
|
||||||
|
"cursor-agent": { # Matches the actual executable name
|
||||||
|
"name": "Cursor",
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- **`scripts/bash/update-agent-context.sh`** — add a file-path variable and a case in `update_specific_agent()`.
|
# No special cases needed - just use agent_key directly!
|
||||||
- **`scripts/powershell/update-agent-context.ps1`** — add a file-path variable, a switch case in `Update-SpecificAgent`, and an entry in `Update-AllExistingAgents`.
|
|
||||||
|
|
||||||
### 5. Test it
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install into a test project
|
|
||||||
specify init my-project --integration <key>
|
|
||||||
|
|
||||||
# Verify files were created
|
|
||||||
ls -R my-project/<commands_dir>/
|
|
||||||
|
|
||||||
# Uninstall cleanly
|
|
||||||
cd my-project && specify integration uninstall <key>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Each integration also has a dedicated test file at `tests/integrations/test_integration_<key>.py`. Run it with:
|
**Benefits of this approach:**
|
||||||
|
|
||||||
```bash
|
- Eliminates special-case logic scattered throughout the codebase
|
||||||
pytest tests/integrations/test_integration_<key>.py -v
|
- Makes the code more maintainable and easier to understand
|
||||||
```
|
- Reduces the chance of bugs when adding new agents
|
||||||
|
- Tool checking "just works" without additional mappings
|
||||||
|
|
||||||
### 6. Optional overrides
|
#### 7. Update Devcontainer files (Optional)
|
||||||
|
|
||||||
The base classes handle most work automatically. Override only when the agent deviates from standard patterns:
|
|
||||||
|
|
||||||
| Override | When to use | Example |
|
|
||||||
|---|---|---|
|
|
||||||
| `command_filename(template_name)` | Custom file naming or extension | Copilot → `speckit.{name}.agent.md` |
|
|
||||||
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag |
|
|
||||||
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` |
|
|
||||||
| `teardown()` | Custom uninstall logic | Rarely needed; base handles manifest-tracked files |
|
|
||||||
|
|
||||||
**Example — Copilot (fully custom `setup`):**
|
|
||||||
|
|
||||||
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
|
|
||||||
|
|
||||||
### 7. Update Devcontainer files (Optional)
|
|
||||||
|
|
||||||
For agents that have VS Code extensions or require CLI installation, update the devcontainer configuration files:
|
For agents that have VS Code extensions or require CLI installation, update the devcontainer configuration files:
|
||||||
|
|
||||||
#### VS Code Extension-based Agents
|
##### VS Code Extension-based Agents
|
||||||
|
|
||||||
For agents available as VS Code extensions, add them to `.devcontainer/devcontainer.json`:
|
For agents available as VS Code extensions, add them to `.devcontainer/devcontainer.json`:
|
||||||
|
|
||||||
```jsonc
|
```json
|
||||||
{
|
{
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
// ... existing extensions ...
|
// ... existing extensions ...
|
||||||
|
// [New Agent Name]
|
||||||
"[New Agent Extension ID]"
|
"[New Agent Extension ID]"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -289,7 +283,7 @@ For agents available as VS Code extensions, add them to `.devcontainer/devcontai
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### CLI-based Agents
|
##### CLI-based Agents
|
||||||
|
|
||||||
For agents that require CLI tools, add installation commands to `.devcontainer/post-create.sh`:
|
For agents that require CLI tools, add installation commands to `.devcontainer/post-create.sh`:
|
||||||
|
|
||||||
@@ -299,16 +293,50 @@ For agents that require CLI tools, add installation commands to `.devcontainer/p
|
|||||||
# Existing installations...
|
# Existing installations...
|
||||||
|
|
||||||
echo -e "\n🤖 Installing [New Agent Name] CLI..."
|
echo -e "\n🤖 Installing [New Agent Name] CLI..."
|
||||||
# run_command "npm install -g [agent-cli-package]@latest"
|
# run_command "npm install -g [agent-cli-package]@latest" # Example for node-based CLI
|
||||||
|
# or other installation instructions (must be non-interactive and compatible with Linux Debian "Trixie" or later)...
|
||||||
echo "✅ Done"
|
echo "✅ Done"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
**Quick Tips:**
|
||||||
|
|
||||||
|
- **Extension-based agents**: Add to the `extensions` array in `devcontainer.json`
|
||||||
|
- **CLI-based agents**: Add installation scripts to `post-create.sh`
|
||||||
|
- **Hybrid agents**: May require both extension and CLI installation
|
||||||
|
- **Test thoroughly**: Ensure installations work in the devcontainer environment
|
||||||
|
|
||||||
|
## Agent Categories
|
||||||
|
|
||||||
|
### CLI-Based Agents
|
||||||
|
|
||||||
|
Require a command-line tool to be installed:
|
||||||
|
|
||||||
|
- **Claude Code**: `claude` CLI
|
||||||
|
- **Gemini CLI**: `gemini` CLI
|
||||||
|
- **Cursor**: `cursor-agent` CLI
|
||||||
|
- **Qwen Code**: `qwen` CLI
|
||||||
|
- **opencode**: `opencode` CLI
|
||||||
|
- **Kiro CLI**: `kiro-cli` CLI
|
||||||
|
- **CodeBuddy CLI**: `codebuddy` CLI
|
||||||
|
- **Qoder CLI**: `qodercli` CLI
|
||||||
|
- **Amp**: `amp` CLI
|
||||||
|
- **SHAI**: `shai` CLI
|
||||||
|
|
||||||
|
### IDE-Based Agents
|
||||||
|
|
||||||
|
Work within integrated development environments:
|
||||||
|
|
||||||
|
- **GitHub Copilot**: Built into VS Code/compatible editors
|
||||||
|
- **Windsurf**: Built into Windsurf IDE
|
||||||
|
- **IBM Bob**: Built into IBM Bob IDE
|
||||||
|
|
||||||
## Command File Formats
|
## Command File Formats
|
||||||
|
|
||||||
### Markdown Format
|
### Markdown Format
|
||||||
|
|
||||||
|
Used by: Claude, Cursor, opencode, Windsurf, Kiro CLI, Amp, SHAI, IBM Bob
|
||||||
|
|
||||||
**Standard format:**
|
**Standard format:**
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
@@ -332,6 +360,8 @@ Command content with {SCRIPT} and $ARGUMENTS placeholders.
|
|||||||
|
|
||||||
### TOML Format
|
### TOML Format
|
||||||
|
|
||||||
|
Used by: Gemini, Qwen
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
description = "Command description"
|
description = "Command description"
|
||||||
|
|
||||||
@@ -340,6 +370,14 @@ Command content with {SCRIPT} and {{args}} placeholders.
|
|||||||
"""
|
"""
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Directory Conventions
|
||||||
|
|
||||||
|
- **CLI agents**: Usually `.<agent-name>/commands/`
|
||||||
|
- **IDE agents**: Follow IDE-specific patterns:
|
||||||
|
- Copilot: `.github/agents/`
|
||||||
|
- Cursor: `.cursor/commands/`
|
||||||
|
- Windsurf: `.windsurf/workflows/`
|
||||||
|
|
||||||
## Argument Patterns
|
## Argument Patterns
|
||||||
|
|
||||||
Different agents use different argument placeholders:
|
Different agents use different argument placeholders:
|
||||||
@@ -349,14 +387,33 @@ Different agents use different argument placeholders:
|
|||||||
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
|
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
|
||||||
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
|
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
|
||||||
|
|
||||||
|
## Testing New Agent Integration
|
||||||
|
|
||||||
|
1. **Build test**: Run package creation script locally
|
||||||
|
2. **CLI test**: Test `specify init --ai <agent>` command
|
||||||
|
3. **File generation**: Verify correct directory structure and files
|
||||||
|
4. **Command validation**: Ensure generated commands work with the agent
|
||||||
|
5. **Context update**: Test agent context update scripts
|
||||||
|
|
||||||
## Common Pitfalls
|
## Common Pitfalls
|
||||||
|
|
||||||
1. **Using shorthand keys instead of actual CLI tool names**: The integration `key` must match the executable name (e.g., `"cursor-agent"` not `"cursor"`). `shutil.which(key)` is used for CLI tool checks — mismatches require special-case mappings.
|
1. **Using shorthand keys instead of actual CLI tool names**: Always use the actual executable name as the AGENT_CONFIG key (e.g., `"cursor-agent"` not `"cursor"`). This prevents the need for special-case mappings throughout the codebase.
|
||||||
2. **Forgetting update scripts**: Both bash and PowerShell thin wrappers and the shared context-update scripts must be updated.
|
2. **Forgetting update scripts**: Both bash and PowerShell scripts must be updated when adding new agents.
|
||||||
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that have a CLI tool; set to `False` for IDE-based agents.
|
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that actually have CLI tools to check; set to `False` for IDE-based agents.
|
||||||
4. **Wrong argument format**: Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML agents.
|
4. **Wrong argument format**: Use correct placeholder format for each agent type (`$ARGUMENTS` for Markdown, `{{args}}` for TOML).
|
||||||
5. **Skipping registration**: The import and `_register()` call in `_register_builtins()` must both be added.
|
5. **Directory naming**: Follow agent-specific conventions exactly (check existing agents for patterns).
|
||||||
|
6. **Help text inconsistency**: Update all user-facing text consistently (help strings, docstrings, README, error messages).
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
When adding new agents:
|
||||||
|
|
||||||
|
- Consider the agent's native command/workflow patterns
|
||||||
|
- Ensure compatibility with the Spec-Driven Development process
|
||||||
|
- Document any special requirements or limitations
|
||||||
|
- Update this guide with lessons learned
|
||||||
|
- Verify the actual CLI tool name before adding to AGENT_CONFIG
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This documentation should be updated whenever new integrations are added to maintain accuracy and completeness.*
|
*This documentation should be updated whenever new agents are added to maintain accuracy and completeness.*
|
||||||
|
|||||||
1121
CHANGELOG.md
1121
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler
|
|||||||
> If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed.
|
> If your pull request introduces a large change that materially impacts the work of the CLI or the rest of the repository (e.g., you're introducing new templates, arguments, or otherwise major changes), make sure that it was **discussed and agreed upon** by the project maintainers. Pull requests with large changes that did not have a prior conversation and agreement will be closed.
|
||||||
|
|
||||||
1. Fork and clone the repository
|
1. Fork and clone the repository
|
||||||
1. Configure and install the dependencies: `uv sync --extra test`
|
1. Configure and install the dependencies: `uv sync`
|
||||||
1. Make sure the CLI works on your machine: `uv run specify --help`
|
1. Make sure the CLI works on your machine: `uv run specify --help`
|
||||||
1. Create a new branch: `git checkout -b my-branch-name`
|
1. Create a new branch: `git checkout -b my-branch-name`
|
||||||
1. Make your change, add tests, and make sure everything still works
|
1. Make your change, add tests, and make sure everything still works
|
||||||
@@ -44,9 +44,6 @@ On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler
|
|||||||
1. Push to your fork and submit a pull request
|
1. Push to your fork and submit a pull request
|
||||||
1. Wait for your pull request to be reviewed and merged.
|
1. Wait for your pull request to be reviewed and merged.
|
||||||
|
|
||||||
For the detailed test workflow, command-selection prompt, and PR reporting template, see [`TESTING.md`](./TESTING.md).
|
|
||||||
Activate the project virtual environment (see the Setup block in [`TESTING.md`](./TESTING.md)), then install the CLI from your working tree (`uv pip install -e .` after `uv sync --extra test`) or otherwise ensure the shell uses the local `specify` binary before running the manual slash-command tests described below.
|
|
||||||
|
|
||||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||||
|
|
||||||
- Follow the project's coding conventions.
|
- Follow the project's coding conventions.
|
||||||
@@ -65,14 +62,6 @@ When working on spec-kit:
|
|||||||
3. Test script functionality in the `scripts/` directory
|
3. Test script functionality in the `scripts/` directory
|
||||||
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
|
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
|
||||||
|
|
||||||
### Recommended validation flow
|
|
||||||
|
|
||||||
For the smoothest review experience, validate changes in this order:
|
|
||||||
|
|
||||||
1. **Run focused automated checks first** — use the quick verification commands in [`TESTING.md`](./TESTING.md) to catch packaging, scaffolding, and configuration regressions early.
|
|
||||||
2. **Run manual workflow tests second** — if your change affects slash commands or the developer workflow, follow [`TESTING.md`](./TESTING.md) to choose the right commands, run them in an agent, and capture results for your PR.
|
|
||||||
3. **Use local release packages when debugging packaged output** — if you need to inspect the exact files CI-style packaging produces, generate local release packages as described below.
|
|
||||||
|
|
||||||
### Testing template and command changes locally
|
### Testing template and command changes locally
|
||||||
|
|
||||||
Running `uv run specify init` pulls released packages, which won’t include your local changes.
|
Running `uv run specify init` pulls released packages, which won’t include your local changes.
|
||||||
@@ -96,8 +85,6 @@ To test your templates, commands, and other changes locally, follow these steps:
|
|||||||
|
|
||||||
Navigate to your test project folder and open the agent to verify your implementation.
|
Navigate to your test project folder and open the agent to verify your implementation.
|
||||||
|
|
||||||
If you only need to validate generated file structure and content before doing manual agent testing, start with the focused automated checks in [`TESTING.md`](./TESTING.md). Keep this section for the cases where you need to inspect the exact packaged output locally.
|
|
||||||
|
|
||||||
## AI contributions in Spec Kit
|
## AI contributions in Spec Kit
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
289
README.md
289
README.md
@@ -9,7 +9,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/github/spec-kit/releases/latest"><img src="https://img.shields.io/github/v/release/github/spec-kit" alt="Latest Release"/></a>
|
<a href="https://github.com/github/spec-kit/actions/workflows/release.yml"><img src="https://github.com/github/spec-kit/actions/workflows/release.yml/badge.svg" alt="Release"/></a>
|
||||||
<a href="https://github.com/github/spec-kit/stargazers"><img src="https://img.shields.io/github/stars/github/spec-kit?style=social" alt="GitHub stars"/></a>
|
<a href="https://github.com/github/spec-kit/stargazers"><img src="https://img.shields.io/github/stars/github/spec-kit?style=social" alt="GitHub stars"/></a>
|
||||||
<a href="https://github.com/github/spec-kit/blob/main/LICENSE"><img src="https://img.shields.io/github/license/github/spec-kit" alt="License"/></a>
|
<a href="https://github.com/github/spec-kit/blob/main/LICENSE"><img src="https://img.shields.io/github/license/github/spec-kit" alt="License"/></a>
|
||||||
<a href="https://github.github.io/spec-kit/"><img src="https://img.shields.io/badge/docs-GitHub_Pages-blue" alt="Documentation"/></a>
|
<a href="https://github.github.io/spec-kit/"><img src="https://img.shields.io/badge/docs-GitHub_Pages-blue" alt="Documentation"/></a>
|
||||||
@@ -22,13 +22,8 @@
|
|||||||
- [🤔 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)
|
||||||
- [📽️ Video Overview](#️-video-overview)
|
- [📽️ Video Overview](#️-video-overview)
|
||||||
- [🧩 Community Extensions](#-community-extensions)
|
|
||||||
- [🎨 Community Presets](#-community-presets)
|
|
||||||
- [🚶 Community Walkthroughs](#-community-walkthroughs)
|
|
||||||
- [🛠️ Community Friends](#️-community-friends)
|
|
||||||
- [🤖 Supported AI Agents](#-supported-ai-agents)
|
- [🤖 Supported AI Agents](#-supported-ai-agents)
|
||||||
- [🔧 Specify CLI Reference](#-specify-cli-reference)
|
- [🔧 Specify CLI Reference](#-specify-cli-reference)
|
||||||
- [🧩 Making Spec Kit Your Own: Extensions & Presets](#-making-spec-kit-your-own-extensions--presets)
|
|
||||||
- [📚 Core Philosophy](#-core-philosophy)
|
- [📚 Core Philosophy](#-core-philosophy)
|
||||||
- [🌟 Development Phases](#-development-phases)
|
- [🌟 Development Phases](#-development-phases)
|
||||||
- [🎯 Experimental Goals](#-experimental-goals)
|
- [🎯 Experimental Goals](#-experimental-goals)
|
||||||
@@ -52,13 +47,9 @@ Choose your preferred installation method:
|
|||||||
|
|
||||||
#### Option 1: Persistent Installation (Recommended)
|
#### Option 1: Persistent Installation (Recommended)
|
||||||
|
|
||||||
Install once and use everywhere. Pin a specific release tag for stability (check [Releases](https://github.com/github/spec-kit/releases) for the latest):
|
Install once and use everywhere:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install a specific stable release (recommended — replace vX.Y.Z with the latest tag)
|
|
||||||
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@vX.Y.Z
|
|
||||||
|
|
||||||
# Or install latest from main (may include unreleased changes)
|
|
||||||
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
|
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,7 +71,7 @@ specify check
|
|||||||
To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed instructions. Quick upgrade:
|
To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed instructions. Quick upgrade:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z
|
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option 2: One-time Usage
|
#### Option 2: One-time Usage
|
||||||
@@ -88,13 +79,7 @@ uv tool install specify-cli --force --from git+https://github.com/github/spec-ki
|
|||||||
Run directly without installing:
|
Run directly without installing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create new project (pinned to a stable release — replace vX.Y.Z with the latest tag)
|
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <PROJECT_NAME>
|
|
||||||
|
|
||||||
# Or initialize in existing project
|
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init . --ai claude
|
|
||||||
# or
|
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --ai claude
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Benefits of persistent installation:**
|
**Benefits of persistent installation:**
|
||||||
@@ -104,13 +89,9 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here
|
|||||||
- Better tool management with `uv tool list`, `uv tool upgrade`, `uv tool uninstall`
|
- Better tool management with `uv tool list`, `uv tool upgrade`, `uv tool uninstall`
|
||||||
- Cleaner shell configuration
|
- Cleaner shell configuration
|
||||||
|
|
||||||
#### Option 3: Enterprise / Air-Gapped Installation
|
|
||||||
|
|
||||||
If your environment blocks access to PyPI or GitHub, see the [Enterprise / Air-Gapped Installation](./docs/installation.md#enterprise--air-gapped-installation) guide for step-by-step instructions on using `pip download` to create portable, OS-specific wheel bundles on a connected machine.
|
|
||||||
|
|
||||||
### 2. Establish project principles
|
### 2. Establish project principles
|
||||||
|
|
||||||
Launch your AI assistant in the project directory. Most agents expose spec-kit as `/speckit.*` slash commands; Codex CLI in skills mode uses `$speckit-*` instead.
|
Launch your AI assistant in the project directory. The `/speckit.*` commands are available in the assistant.
|
||||||
|
|
||||||
Use the **`/speckit.constitution`** command to create your project's governing principles and development guidelines that will guide all subsequent development.
|
Use the **`/speckit.constitution`** command to create your project's governing principles and development guidelines that will guide all subsequent development.
|
||||||
|
|
||||||
@@ -158,121 +139,6 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
|
|||||||
|
|
||||||
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
||||||
|
|
||||||
## 🧩 Community Extensions
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
|
|
||||||
|
|
||||||
🔍 **Browse and search community extensions on the [Community Extensions website](https://speckit-community.github.io/extensions/).**
|
|
||||||
|
|
||||||
The following community-contributed extensions are available in [`catalog.community.json`](extensions/catalog.community.json):
|
|
||||||
|
|
||||||
**Categories:**
|
|
||||||
|
|
||||||
- `docs` — reads, validates, or generates spec artifacts
|
|
||||||
- `code` — reviews, validates, or modifies source code
|
|
||||||
- `process` — orchestrates workflow across phases
|
|
||||||
- `integration` — syncs with external platforms
|
|
||||||
- `visibility` — reports on project health or progress
|
|
||||||
|
|
||||||
**Effect:**
|
|
||||||
|
|
||||||
- `Read-only` — produces reports without modifying files
|
|
||||||
- `Read+Write` — modifies files, creates artifacts, or updates specs
|
|
||||||
|
|
||||||
| Extension | Purpose | Category | Effect | URL |
|
|
||||||
|-----------|---------|----------|--------|-----|
|
|
||||||
| AI-Driven Engineering (AIDE) | A structured 7-step workflow for building new projects from scratch with AI assistants — from vision through implementation | `process` | Read+Write | [aide](https://github.com/mnriem/spec-kit-extensions/tree/main/aide) |
|
|
||||||
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
|
||||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
|
||||||
| Checkpoint Extension | Commit the changes made during the middle of the implementation, so you don't end up with just one very large commit at the end | `code` | Read+Write | [spec-kit-checkpoint](https://github.com/aaronrsun/spec-kit-checkpoint) |
|
|
||||||
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | `code` | Read+Write | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
|
||||||
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
|
||||||
| DocGuard — CDD Enforcement | Canonical-Driven Development enforcement. Validates, scores, and traces project documentation with automated checks, AI-driven workflows, and spec-kit hooks. Zero NPM runtime dependencies. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
|
||||||
| Extensify | Create and validate extensions and extension catalogs | `process` | Read+Write | [extensify](https://github.com/mnriem/spec-kit-extensions/tree/main/extensify) |
|
|
||||||
| Fix Findings | Automated analyze-fix-reanalyze loop that resolves spec findings until clean | `code` | Read+Write | [spec-kit-fix-findings](https://github.com/Quratulain-bilal/spec-kit-fix-findings) |
|
|
||||||
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
|
||||||
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
|
|
||||||
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
|
||||||
| Learning Extension | Generate educational guides from implementations and enhance clarifications with mentoring context | `docs` | Read+Write | [spec-kit-learn](https://github.com/imviancagrace/spec-kit-learn) |
|
|
||||||
| MAQA — Multi-Agent & Quality Assurance | Coordinator → feature → QA agent workflow with parallel worktree-based implementation. Language-agnostic. Auto-detects installed board plugins. Optional CI gate. | `process` | Read+Write | [spec-kit-maqa-ext](https://github.com/GenieRobot/spec-kit-maqa-ext) |
|
|
||||||
| MAQA Azure DevOps Integration | Azure DevOps Boards integration for MAQA — syncs User Stories and Task children as features progress | `integration` | Read+Write | [spec-kit-maqa-azure-devops](https://github.com/GenieRobot/spec-kit-maqa-azure-devops) |
|
|
||||||
| MAQA CI/CD Gate | Auto-detects GitHub Actions, CircleCI, GitLab CI, and Bitbucket Pipelines. Blocks QA handoff until pipeline is green. | `process` | Read+Write | [spec-kit-maqa-ci](https://github.com/GenieRobot/spec-kit-maqa-ci) |
|
|
||||||
| MAQA GitHub Projects Integration | GitHub Projects v2 integration for MAQA — syncs draft issues and Status columns as features progress | `integration` | Read+Write | [spec-kit-maqa-github-projects](https://github.com/GenieRobot/spec-kit-maqa-github-projects) |
|
|
||||||
| MAQA Jira Integration | Jira integration for MAQA — syncs Stories and Subtasks as features progress through the board | `integration` | Read+Write | [spec-kit-maqa-jira](https://github.com/GenieRobot/spec-kit-maqa-jira) |
|
|
||||||
| MAQA Linear Integration | Linear integration for MAQA — syncs issues and sub-issues across workflow states as features progress | `integration` | Read+Write | [spec-kit-maqa-linear](https://github.com/GenieRobot/spec-kit-maqa-linear) |
|
|
||||||
| MAQA Trello Integration | Trello board integration for MAQA — populates board from specs, moves cards, real-time checklist ticking | `integration` | Read+Write | [spec-kit-maqa-trello](https://github.com/GenieRobot/spec-kit-maqa-trello) |
|
|
||||||
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
|
|
||||||
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
|
|
||||||
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
|
|
||||||
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
|
|
||||||
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
|
|
||||||
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
|
|
||||||
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
|
|
||||||
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
|
|
||||||
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
|
|
||||||
| Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) |
|
|
||||||
| Retro Extension | Sprint retrospective analysis with metrics, spec accuracy assessment, and improvement suggestions | `process` | Read+Write | [spec-kit-retro](https://github.com/arunt14/spec-kit-retro) |
|
|
||||||
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | `docs` | Read+Write | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
|
|
||||||
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | `code` | Read-only | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
|
|
||||||
| SDD Utilities | Resume interrupted workflows, validate project health, and verify spec-to-task traceability | `process` | Read+Write | [speckit-utils](https://github.com/mvanhorn/speckit-utils) |
|
|
||||||
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
|
|
||||||
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
|
|
||||||
| Ship Release Extension | Automates release pipeline: pre-flight checks, branch sync, changelog generation, CI verification, and PR creation | `process` | Read+Write | [spec-kit-ship](https://github.com/arunt14/spec-kit-ship) |
|
|
||||||
| Spec Critique Extension | Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives | `docs` | Read-only | [spec-kit-critique](https://github.com/arunt14/spec-kit-critique) |
|
|
||||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
|
||||||
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
|
||||||
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
|
||||||
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
|
||||||
|
|
||||||
To submit your own extension, see the [Extension Publishing Guide](extensions/EXTENSION-PUBLISHING-GUIDE.md).
|
|
||||||
|
|
||||||
## 🎨 Community Presets
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
|
|
||||||
|
|
||||||
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](presets/catalog.community.json):
|
|
||||||
|
|
||||||
| Preset | Purpose | Provides | Requires | URL |
|
|
||||||
|--------|---------|----------|----------|-----|
|
|
||||||
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
|
|
||||||
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
|
|
||||||
|
|
||||||
To build and publish your own preset, see the [Presets Publishing Guide](presets/PUBLISHING.md).
|
|
||||||
|
|
||||||
## 🚶 Community Walkthroughs
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Community walkthroughs are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their content before following along and use at your own discretion.
|
|
||||||
|
|
||||||
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
|
|
||||||
|
|
||||||
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
|
|
||||||
|
|
||||||
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
|
|
||||||
|
|
||||||
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
|
|
||||||
|
|
||||||
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
|
|
||||||
|
|
||||||
- **[Brownfield Go / React dashboard demo](https://github.com/mnriem/spec-kit-go-brownfield-demo)** — Demonstrates spec-kit driven entirely from the **terminal using GitHub Copilot CLI**. Extends NASA's open-source Hermes ground support system (Go) with a lightweight React-based web telemetry dashboard, showing that the full constitution → specify → plan → tasks → implement workflow works from the terminal.
|
|
||||||
|
|
||||||
- **[Greenfield Spring Boot MVC with a custom preset](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)** — Builds a Spring Boot MVC application from scratch using a custom pirate-speak preset, demonstrating how presets can reshape the entire spec-kit experience: specifications become "Voyage Manifests," plans become "Battle Plans," and tasks become "Crew Assignments" — all generated in full pirate vernacular without changing any tooling.
|
|
||||||
|
|
||||||
- **[Greenfield Spring Boot + React with a custom extension](https://github.com/mnriem/spec-kit-aide-extension-demo)** — Walks through the **AIDE extension**, a community extension that adds an alternative spec-driven workflow to spec-kit with high-level specs (vision) and low-level specs (work items) organized in a 7-step iterative lifecycle: vision → roadmap → progress tracking → work queue → work items → execution → feedback loops. Uses a family trading platform (Spring Boot 4, React 19, PostgreSQL, Docker Compose) as the scenario to illustrate how the extension mechanism lets you plug in a different style of spec-driven development without changing any core tooling — truly utilizing the "Kit" in Spec Kit.
|
|
||||||
|
|
||||||
## 🛠️ Community Friends
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Community projects listed here are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
|
|
||||||
|
|
||||||
Community projects that extend, visualize, or build on Spec Kit:
|
|
||||||
|
|
||||||
- **[cc-sdd](https://github.com/rhuss/cc-sdd)** - A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
|
|
||||||
|
|
||||||
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
|
||||||
|
|
||||||
## 🤖 Supported AI Agents
|
## 🤖 Supported AI Agents
|
||||||
|
|
||||||
| Agent | Support | Notes |
|
| Agent | Support | Notes |
|
||||||
@@ -281,9 +147,9 @@ Community projects that extend, visualize, or build on Spec Kit:
|
|||||||
| [Kiro CLI](https://kiro.dev/docs/cli/) | ✅ | Use `--ai kiro-cli` (alias: `--ai kiro`) |
|
| [Kiro CLI](https://kiro.dev/docs/cli/) | ✅ | Use `--ai kiro-cli` (alias: `--ai kiro`) |
|
||||||
| [Amp](https://ampcode.com/) | ✅ | |
|
| [Amp](https://ampcode.com/) | ✅ | |
|
||||||
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
|
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
|
||||||
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | Installs skills in `.claude/skills`; invoke spec-kit as `/speckit-constitution`, `/speckit-plan`, etc. |
|
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | |
|
||||||
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
|
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
|
||||||
| [Codex CLI](https://github.com/openai/codex) | ✅ | Requires `--ai-skills`. Codex recommends [skills](https://developers.openai.com/codex/skills) and treats [custom prompts](https://developers.openai.com/codex/custom-prompts) as deprecated. Spec-kit installs Codex skills into `.agents/skills` and invokes them as `$speckit-<command>`. |
|
| [Codex CLI](https://github.com/openai/codex) | ✅ | |
|
||||||
| [Cursor](https://cursor.sh/) | ✅ | |
|
| [Cursor](https://cursor.sh/) | ✅ | |
|
||||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
|
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
|
||||||
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
|
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
|
||||||
@@ -291,18 +157,11 @@ Community projects that extend, visualize, or build on Spec Kit:
|
|||||||
| [Jules](https://jules.google.com/) | ✅ | |
|
| [Jules](https://jules.google.com/) | ✅ | |
|
||||||
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | ✅ | |
|
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | ✅ | |
|
||||||
| [opencode](https://opencode.ai/) | ✅ | |
|
| [opencode](https://opencode.ai/) | ✅ | |
|
||||||
| [Pi Coding Agent](https://pi.dev) | ✅ | Pi doesn't have MCP support out of the box, so `taskstoissues` won't work as intended. MCP support can be added via [extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent#extensions) |
|
|
||||||
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
|
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
|
||||||
| [Roo Code](https://roocode.com/) | ✅ | |
|
| [Roo Code](https://roocode.com/) | ✅ | |
|
||||||
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
|
||||||
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | ✅ | |
|
|
||||||
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | ✅ | |
|
|
||||||
| [Kimi Code](https://code.kimi.com/) | ✅ | |
|
|
||||||
| [iFlow CLI](https://docs.iflow.cn/en/cli/quickstart) | ✅ | |
|
|
||||||
| [Windsurf](https://windsurf.com/) | ✅ | |
|
| [Windsurf](https://windsurf.com/) | ✅ | |
|
||||||
| [Junie](https://junie.jetbrains.com/) | ✅ | |
|
| [Antigravity (agy)](https://agy.ai/) | ✅ | |
|
||||||
| [Antigravity (agy)](https://antigravity.google/) | ✅ | Requires `--ai-skills` |
|
|
||||||
| [Trae](https://www.trae.ai/) | ✅ | |
|
|
||||||
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
|
||||||
|
|
||||||
## 🔧 Specify CLI Reference
|
## 🔧 Specify CLI Reference
|
||||||
@@ -311,28 +170,27 @@ The `specify` command supports the following options:
|
|||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
| ------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `init` | Initialize a new Specify project from the latest template |
|
| `init` | Initialize a new Specify project from the latest template |
|
||||||
| `check` | Check for installed tools: `git` plus all CLI-based agents configured in `AGENT_CONFIG` (for example: `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `junie`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, etc.) |
|
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `kiro-cli`, `shai`, `qodercli`) |
|
||||||
|
|
||||||
### `specify init` Arguments & Options
|
### `specify init` Arguments & Options
|
||||||
|
|
||||||
| Argument/Option | Type | Description |
|
| Argument/Option | Type | Description |
|
||||||
| ---------------------- | -------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
|
||||||
| `--ai` | Option | AI assistant to use (see `AGENT_CONFIG` for the full, up-to-date list). Common options include: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `junie`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, `vibe`, `kimi`, `iflow`, `pi`, or `generic` (requires `--ai-commands-dir`) |
|
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `kiro-cli` (`kiro` alias), `agy`, `bob`, `qodercli`, or `generic` (requires `--ai-commands-dir`) |
|
||||||
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
| `--ai-commands-dir` | Option | Directory for agent command files (required with `--ai generic`, e.g. `.myagent/commands/`) |
|
||||||
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
| `--script` | Option | Script variant to use: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||||
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
| `--ignore-agent-tools` | Flag | Skip checks for AI agent tools like Claude Code |
|
||||||
| `--no-git` | Flag | Skip git repository initialization |
|
| `--no-git` | Flag | Skip git repository initialization |
|
||||||
| `--here` | Flag | Initialize project in the current directory instead of creating a new one |
|
| `--here` | Flag | Initialize project in the current directory instead of creating a new one |
|
||||||
| `--force` | Flag | Force merge/overwrite when initializing in current directory (skip confirmation) |
|
| `--force` | Flag | Force merge/overwrite when initializing in current directory (skip confirmation) |
|
||||||
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
|
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
|
||||||
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
|
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
|
||||||
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
|
| `--github-token` | Option | GitHub token for API requests (or set GH_TOKEN/GITHUB_TOKEN env variable) |
|
||||||
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`). Extension commands are also auto-registered as skills when extensions are added later. |
|
| `--ai-skills` | Flag | Install Prompt.MD templates as agent skills in agent-specific `skills/` directory (requires `--ai`) |
|
||||||
| `--branch-numbering` | Option | Branch numbering strategy: `sequential` (default — `001`, `002`, `003`) or `timestamp` (`YYYYMMDD-HHMMSS`). Timestamp mode is useful for distributed teams to avoid numbering conflicts |
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
@@ -361,21 +219,9 @@ specify init my-project --ai amp
|
|||||||
# Initialize with SHAI support
|
# Initialize with SHAI support
|
||||||
specify init my-project --ai shai
|
specify init my-project --ai shai
|
||||||
|
|
||||||
# Initialize with Mistral Vibe support
|
|
||||||
specify init my-project --ai vibe
|
|
||||||
|
|
||||||
# Initialize with IBM Bob support
|
# Initialize with IBM Bob support
|
||||||
specify init my-project --ai bob
|
specify init my-project --ai bob
|
||||||
|
|
||||||
# Initialize with Pi Coding Agent support
|
|
||||||
specify init my-project --ai pi
|
|
||||||
|
|
||||||
# Initialize with Codex CLI support
|
|
||||||
specify init my-project --ai codex --ai-skills
|
|
||||||
|
|
||||||
# Initialize with Antigravity support
|
|
||||||
specify init my-project --ai agy --ai-skills
|
|
||||||
|
|
||||||
# Initialize with an unsupported agent (generic / bring your own agent)
|
# Initialize with an unsupported agent (generic / bring your own agent)
|
||||||
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
specify init my-project --ai generic --ai-commands-dir .myagent/commands/
|
||||||
|
|
||||||
@@ -401,28 +247,19 @@ specify init my-project --ai claude --debug
|
|||||||
# Use GitHub token for API requests (helpful for corporate environments)
|
# Use GitHub token for API requests (helpful for corporate environments)
|
||||||
specify init my-project --ai claude --github-token ghp_your_token_here
|
specify init my-project --ai claude --github-token ghp_your_token_here
|
||||||
|
|
||||||
# Claude Code installs skills with the project by default
|
# Install agent skills with the project
|
||||||
specify init my-project --ai claude
|
specify init my-project --ai claude --ai-skills
|
||||||
|
|
||||||
# Initialize in current directory with agent skills
|
# Initialize in current directory with agent skills
|
||||||
specify init --here --ai gemini --ai-skills
|
specify init --here --ai gemini --ai-skills
|
||||||
|
|
||||||
# Use timestamp-based branch numbering (useful for distributed teams)
|
|
||||||
specify init my-project --ai claude --branch-numbering timestamp
|
|
||||||
|
|
||||||
# Check system requirements
|
# Check system requirements
|
||||||
specify check
|
specify check
|
||||||
```
|
```
|
||||||
|
|
||||||
### Available Slash Commands
|
### Available Slash Commands
|
||||||
|
|
||||||
After running `specify init`, your AI coding agent will have access to these structured development commands.
|
After running `specify init`, your AI coding agent will have access to these slash commands for structured development:
|
||||||
|
|
||||||
Most agents expose the traditional dotted slash commands shown below, like `/speckit.plan`.
|
|
||||||
|
|
||||||
Claude Code installs spec-kit as skills and invokes them as `/speckit-constitution`, `/speckit-specify`, `/speckit-plan`, `/speckit-tasks`, and `/speckit-implement`.
|
|
||||||
|
|
||||||
For Codex CLI, `--ai-skills` installs spec-kit as agent skills instead of slash-command prompt files. In Codex skills mode, invoke spec-kit as `$speckit-constitution`, `$speckit-specify`, `$speckit-plan`, `$speckit-tasks`, and `$speckit-implement`.
|
|
||||||
|
|
||||||
#### Core Commands
|
#### Core Commands
|
||||||
|
|
||||||
@@ -452,68 +289,6 @@ Additional commands for enhanced quality and validation:
|
|||||||
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches.<br/>\*\*Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. |
|
| `SPECIFY_FEATURE` | Override feature detection for non-Git repositories. Set to the feature directory name (e.g., `001-photo-albums`) to work on a specific feature when not using Git branches.<br/>\*\*Must be set in the context of the agent you're working with prior to using `/speckit.plan` or follow-up commands. |
|
||||||
|
|
||||||
## 🧩 Making Spec Kit Your Own: Extensions & Presets
|
|
||||||
|
|
||||||
Spec Kit can be tailored to your needs through two complementary systems — **extensions** and **presets** — plus project-local overrides for one-off adjustments:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
block-beta
|
|
||||||
columns 1
|
|
||||||
overrides["⬆ Highest priority\nProject-Local Overrides\n.specify/templates/overrides/"]
|
|
||||||
presets["Presets — Customize core & extensions\n.specify/presets/<preset-id>/templates/"]
|
|
||||||
extensions["Extensions — Add new capabilities\n.specify/extensions/<ext-id>/templates/"]
|
|
||||||
core["Spec Kit Core — Built-in SDD commands & templates\n.specify/templates/\n⬇ Lowest priority"]
|
|
||||||
|
|
||||||
style overrides fill:transparent,stroke:#999
|
|
||||||
style presets fill:transparent,stroke:#4a9eda
|
|
||||||
style extensions fill:transparent,stroke:#4a9e4a
|
|
||||||
style core fill:transparent,stroke:#e6a817
|
|
||||||
```
|
|
||||||
|
|
||||||
**Templates** are resolved at **runtime** — Spec Kit walks the stack top-down and uses the first match. Project-local overrides (`.specify/templates/overrides/`) let you make one-off adjustments for a single project without creating a full preset. **Commands** are applied at **install time** — when you run `specify extension add` or `specify preset add`, command files are written into agent directories (e.g., `.claude/commands/`). If multiple presets or extensions provide the same command, the highest-priority version wins. On removal, the next-highest-priority version is restored automatically. If no overrides or customizations exist, Spec Kit uses its core defaults.
|
|
||||||
|
|
||||||
### Extensions — Add New Capabilities
|
|
||||||
|
|
||||||
Use **extensions** when you need functionality that goes beyond Spec Kit's core. Extensions introduce new commands and templates — for example, adding domain-specific workflows that are not covered by the built-in SDD commands, integrating with external tools, or adding entirely new development phases. They expand *what Spec Kit can do*.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Search available extensions
|
|
||||||
specify extension search
|
|
||||||
|
|
||||||
# Install an extension
|
|
||||||
specify extension add <extension-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, extensions could add Jira integration, post-implementation code review, V-Model test traceability, or project health diagnostics.
|
|
||||||
|
|
||||||
See the [Extensions README](./extensions/README.md) for the full guide and how to build and publish your own. Browse the [community extensions](#-community-extensions) above for what's available.
|
|
||||||
|
|
||||||
### Presets — Customize Existing Workflows
|
|
||||||
|
|
||||||
Use **presets** when you want to change *how* Spec Kit works without adding new capabilities. Presets override the templates and commands that ship with the core *and* with installed extensions — for example, enforcing a compliance-oriented spec format, using domain-specific terminology, or applying organizational standards to plans and tasks. They customize the artifacts and instructions that Spec Kit and its extensions produce.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Search available presets
|
|
||||||
specify preset search
|
|
||||||
|
|
||||||
# Install a preset
|
|
||||||
specify preset add <preset-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, presets could restructure spec templates to require regulatory traceability, adapt the workflow to fit the methodology you use (e.g., Agile, Kanban, Waterfall, jobs-to-be-done, or domain-driven design), add mandatory security review gates to plans, enforce test-first task ordering, or localize the entire workflow to a different language. The [pirate-speak demo](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo) shows just how deep the customization can go. Multiple presets can be stacked with priority ordering.
|
|
||||||
|
|
||||||
See the [Presets README](./presets/README.md) for the full guide, including resolution order, priority, and how to create your own.
|
|
||||||
|
|
||||||
### When to Use Which
|
|
||||||
|
|
||||||
| Goal | Use |
|
|
||||||
| --- | --- |
|
|
||||||
| Add a brand-new command or workflow | Extension |
|
|
||||||
| Customize the format of specs, plans, or tasks | Preset |
|
|
||||||
| Integrate an external tool or service | Extension |
|
|
||||||
| Enforce organizational or regulatory standards | Preset |
|
|
||||||
| Ship reusable domain-specific templates | Either — presets for template overrides, extensions for templates bundled with new commands |
|
|
||||||
|
|
||||||
## 📚 Core Philosophy
|
## 📚 Core Philosophy
|
||||||
|
|
||||||
Spec-Driven Development is a structured process that emphasizes:
|
Spec-Driven Development is a structured process that emphasizes:
|
||||||
@@ -608,11 +383,11 @@ specify init <project_name> --ai copilot
|
|||||||
|
|
||||||
# Or in current directory:
|
# Or in current directory:
|
||||||
specify init . --ai claude
|
specify init . --ai claude
|
||||||
specify init . --ai codex --ai-skills
|
specify init . --ai codex
|
||||||
|
|
||||||
# or use --here flag
|
# or use --here flag
|
||||||
specify init --here --ai claude
|
specify init --here --ai claude
|
||||||
specify init --here --ai codex --ai-skills
|
specify init --here --ai codex
|
||||||
|
|
||||||
# Force merge into a non-empty current directory
|
# Force merge into a non-empty current directory
|
||||||
specify init . --force --ai claude
|
specify init . --force --ai claude
|
||||||
@@ -621,7 +396,7 @@ specify init . --force --ai claude
|
|||||||
specify init --here --force --ai claude
|
specify init --here --force --ai claude
|
||||||
```
|
```
|
||||||
|
|
||||||
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, or Kiro CLI installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify init <project_name> --ai claude --ignore-agent-tools
|
specify init <project_name> --ai claude --ignore-agent-tools
|
||||||
|
|||||||
13
SUPPORT.md
13
SUPPORT.md
@@ -1,17 +1,18 @@
|
|||||||
# Support
|
# Support
|
||||||
|
|
||||||
## How to get help
|
## How to file issues and get help
|
||||||
|
|
||||||
Please search existing [issues](https://github.com/github/spec-kit/issues) and [discussions](https://github.com/github/spec-kit/discussions) before creating new ones to avoid duplicates.
|
This project uses GitHub issues to track bugs and feature requests. Please search the existing issues before filing new issues to avoid duplicates. For new issues, file your bug or feature request as a new issue.
|
||||||
|
|
||||||
- Review the [README](./README.md) for getting started instructions and troubleshooting tips
|
For help or questions about using this project, please:
|
||||||
|
|
||||||
|
- Open a [GitHub issue](https://github.com/github/spec-kit/issues/new) for bug reports, feature requests, or questions about the Spec-Driven Development methodology
|
||||||
- Check the [comprehensive guide](./spec-driven.md) for detailed documentation on the Spec-Driven Development process
|
- Check the [comprehensive guide](./spec-driven.md) for detailed documentation on the Spec-Driven Development process
|
||||||
- Ask in [GitHub Discussions](https://github.com/github/spec-kit/discussions) for questions about using Spec Kit or the Spec-Driven Development methodology
|
- Review the [README](./README.md) for getting started instructions and troubleshooting tips
|
||||||
- Open a [GitHub issue](https://github.com/github/spec-kit/issues/new) for bug reports and feature requests
|
|
||||||
|
|
||||||
## Project Status
|
## Project Status
|
||||||
|
|
||||||
**Spec Kit** is under active development and maintained by GitHub staff and the community. We will do our best to respond to support, feature requests, and community questions as time permits.
|
**Spec Kit** is under active development and maintained by GitHub staff **AND THE COMMUNITY**. We will do our best to respond to support, feature requests, and community questions in a timely manner.
|
||||||
|
|
||||||
## GitHub Support Policy
|
## GitHub Support Policy
|
||||||
|
|
||||||
|
|||||||
133
TESTING.md
133
TESTING.md
@@ -1,133 +0,0 @@
|
|||||||
# Testing Guide
|
|
||||||
|
|
||||||
This document is the detailed testing companion to [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
|
||||||
|
|
||||||
Use it for three things:
|
|
||||||
|
|
||||||
1. running quick automated checks before manual testing,
|
|
||||||
2. manually testing affected slash commands through an AI agent, and
|
|
||||||
3. capturing the results in a PR-friendly format.
|
|
||||||
|
|
||||||
Any change that affects a slash command's behavior requires manually testing that command through an AI agent and submitting results with the PR.
|
|
||||||
|
|
||||||
## Recommended order
|
|
||||||
|
|
||||||
1. **Sync your environment** — install the project and test dependencies.
|
|
||||||
2. **Run focused automated checks** — especially for packaging, scaffolding, agent config, and generated-file changes.
|
|
||||||
3. **Run manual agent tests** — for any affected slash commands.
|
|
||||||
4. **Paste results into your PR** — include both command-selection reasoning and manual test results.
|
|
||||||
|
|
||||||
## Quick automated checks
|
|
||||||
|
|
||||||
Run these before manual testing when your change affects packaging, scaffolding, templates, release artifacts, or agent wiring.
|
|
||||||
|
|
||||||
### Environment setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd <spec-kit-repo>
|
|
||||||
uv sync --extra test
|
|
||||||
source .venv/bin/activate # On Windows (CMD): .venv\Scripts\activate | (PowerShell): .venv\Scripts\Activate.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generated package structure and content
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python -m pytest tests/test_core_pack_scaffold.py -q
|
|
||||||
```
|
|
||||||
|
|
||||||
This validates the generated files that CI-style packaging depends on, including directory layout, file names, frontmatter/TOML validity, placeholder replacement, `.specify/` path rewrites, and parity with `create-release-packages.sh`.
|
|
||||||
|
|
||||||
### Agent configuration and release wiring consistency
|
|
||||||
|
|
||||||
```bash
|
|
||||||
uv run python -m pytest tests/test_agent_config_consistency.py -q
|
|
||||||
```
|
|
||||||
|
|
||||||
Run this when you change agent metadata, release scripts, context update scripts, or artifact naming.
|
|
||||||
|
|
||||||
### Optional single-agent packaging spot check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
AGENTS=copilot SCRIPTS=sh ./.github/workflows/scripts/create-release-packages.sh v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
Inspect `.genreleases/sdd-copilot-package-sh/` and the matching ZIP in `.genreleases/` when you want to review the exact packaged output for one agent/script combination.
|
|
||||||
|
|
||||||
## Manual testing process
|
|
||||||
|
|
||||||
1. **Identify affected commands** — use the [prompt below](#determining-which-tests-to-run) to have your agent analyze your changed files and determine which commands need testing.
|
|
||||||
2. **Set up a test project** — scaffold from your local branch (see [Setup](#setup)).
|
|
||||||
3. **Run each affected command** — invoke it in your agent, verify it completes successfully, and confirm it produces the expected output (files created, scripts executed, artifacts populated).
|
|
||||||
4. **Run prerequisites first** — commands that depend on earlier commands (e.g., `/speckit.tasks` requires `/speckit.plan` which requires `/speckit.specify`) must be run in order.
|
|
||||||
5. **Report results** — paste the [reporting template](#reporting-results) into your PR with pass/fail for each command tested.
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install the project and test dependencies from your local branch
|
|
||||||
cd <spec-kit-repo>
|
|
||||||
uv sync --extra test
|
|
||||||
source .venv/bin/activate # On Windows (CMD): .venv\Scripts\activate | (PowerShell): .venv\Scripts\Activate.ps1
|
|
||||||
uv pip install -e .
|
|
||||||
# Ensure the `specify` binary in this environment points at your working tree so the agent runs the branch you're testing.
|
|
||||||
|
|
||||||
# Initialize a test project using your local changes
|
|
||||||
uv run specify init /tmp/speckit-test --ai <agent> --offline
|
|
||||||
cd /tmp/speckit-test
|
|
||||||
|
|
||||||
# Open in your agent
|
|
||||||
```
|
|
||||||
|
|
||||||
If you are testing the packaged output rather than the live source tree, create a local release package first as described in [`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
|
||||||
|
|
||||||
## Reporting results
|
|
||||||
|
|
||||||
Paste this into your PR:
|
|
||||||
|
|
||||||
~~~markdown
|
|
||||||
## Manual test results
|
|
||||||
|
|
||||||
**Agent**: [e.g., GitHub Copilot in VS Code] | **OS/Shell**: [e.g., macOS/zsh]
|
|
||||||
|
|
||||||
| Command tested | Notes |
|
|
||||||
|----------------|-------|
|
|
||||||
| `/speckit.command` | |
|
|
||||||
~~~
|
|
||||||
|
|
||||||
## Determining which tests to run
|
|
||||||
|
|
||||||
Copy this prompt into your agent. Include the agent's response (selected tests plus a brief explanation of the mapping) in your PR.
|
|
||||||
|
|
||||||
~~~text
|
|
||||||
Read TESTING.md, then run `git diff --name-only main` to get my changed files.
|
|
||||||
For each changed file, determine which slash commands it affects by reading
|
|
||||||
the command templates in templates/commands/ to understand what each command
|
|
||||||
invokes. Use these mapping rules:
|
|
||||||
|
|
||||||
- templates/commands/X.md → the command it defines
|
|
||||||
- scripts/bash/Y.sh or scripts/powershell/Y.ps1 → every command that invokes that script (grep templates/commands/ for the script name). Also check transitive dependencies: if the changed script is sourced by other scripts (e.g., common.sh is sourced by create-new-feature.sh, check-prerequisites.sh, setup-plan.sh, update-agent-context.sh), then every command invoking those downstream scripts is also affected
|
|
||||||
- templates/Z-template.md → every command that consumes that template during execution
|
|
||||||
- src/specify_cli/*.py → CLI commands (`specify init`, `specify check`, `specify extension *`, `specify preset *`); test the affected CLI command and, for init/scaffolding changes, at minimum test /speckit.specify
|
|
||||||
- extensions/X/commands/* → the extension command it defines
|
|
||||||
- extensions/X/scripts/* → every extension command that invokes that script
|
|
||||||
- extensions/X/extension.yml or config-template.yml → every command in that extension. Also check if the manifest defines hooks (look for `hooks:` entries like `before_specify`, `after_implement`, etc.) — if so, the core commands those hooks attach to are also affected
|
|
||||||
- presets/*/* → test preset scaffolding via `specify init` with the preset
|
|
||||||
- pyproject.toml → packaging/bundling; test `specify init` and verify bundled assets
|
|
||||||
|
|
||||||
Include prerequisite tests (e.g., T5 requires T3 requires T1).
|
|
||||||
|
|
||||||
Output in this format:
|
|
||||||
|
|
||||||
### Test selection reasoning
|
|
||||||
|
|
||||||
| Changed file | Affects | Test | Why |
|
|
||||||
|---|---|---|---|
|
|
||||||
| (path) | (command) | T# | (reason) |
|
|
||||||
|
|
||||||
### Required tests
|
|
||||||
|
|
||||||
Number each test sequentially (T1, T2, ...). List prerequisite tests first.
|
|
||||||
|
|
||||||
- T1: /speckit.command — (reason)
|
|
||||||
- T2: /speckit.command — (reason)
|
|
||||||
~~~
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Linux/macOS** (or Windows; PowerShell scripts now supported without WSL)
|
- **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/), [Codebuddy CLI](https://www.codebuddy.ai/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Pi Coding Agent](https://pi.dev)
|
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Codebuddy CLI](https://www.codebuddy.ai/cli) 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/)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
@@ -12,22 +12,18 @@
|
|||||||
|
|
||||||
### Initialize a New Project
|
### Initialize a New Project
|
||||||
|
|
||||||
The easiest way to get started is to initialize a new project. Pin a specific release tag for stability (check [Releases](https://github.com/github/spec-kit/releases) for the latest):
|
The easiest way to get started is to initialize a new project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install from a specific stable release (recommended — replace vX.Y.Z with the latest tag)
|
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <PROJECT_NAME>
|
|
||||||
|
|
||||||
# Or install latest from main (may include unreleased changes)
|
|
||||||
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>
|
||||||
```
|
```
|
||||||
|
|
||||||
Or initialize in the current directory:
|
Or initialize in the current directory:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init .
|
uvx --from git+https://github.com/github/spec-kit.git specify init .
|
||||||
# or use the --here flag
|
# or use the --here flag
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here
|
uvx --from git+https://github.com/github/spec-kit.git specify init --here
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specify AI Agent
|
### Specify AI Agent
|
||||||
@@ -35,11 +31,10 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here
|
|||||||
You can proactively specify your AI agent during initialization:
|
You can proactively specify your AI agent during initialization:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai claude
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai claude
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai gemini
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai gemini
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai copilot
|
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@vX.Y.Z specify init <project_name> --ai codebuddy
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai codebuddy
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai pi
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specify Script Type (Shell vs PowerShell)
|
### Specify Script Type (Shell vs PowerShell)
|
||||||
@@ -55,8 +50,8 @@ Auto behavior:
|
|||||||
Force a specific script type:
|
Force a specific script type:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --script sh
|
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@vX.Y.Z specify init <project_name> --script ps
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script ps
|
||||||
```
|
```
|
||||||
|
|
||||||
### Ignore Agent Tools Check
|
### Ignore Agent Tools Check
|
||||||
@@ -64,7 +59,7 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <proje
|
|||||||
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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai claude --ignore-agent-tools
|
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai claude --ignore-agent-tools
|
||||||
```
|
```
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
@@ -79,52 +74,6 @@ The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Enterprise / Air-Gapped Installation
|
|
||||||
|
|
||||||
If your environment blocks access to PyPI (you see 403 errors when running `uv tool install` or `pip install`), you can create a portable wheel bundle on a connected machine and transfer it to the air-gapped target.
|
|
||||||
|
|
||||||
**Step 1: Build the wheel on a connected machine (same OS and Python version as the target)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone https://github.com/github/spec-kit.git
|
|
||||||
cd spec-kit
|
|
||||||
|
|
||||||
# Build the wheel
|
|
||||||
pip install build
|
|
||||||
python -m build --wheel --outdir dist/
|
|
||||||
|
|
||||||
# Download the wheel and all its runtime dependencies
|
|
||||||
pip download -d dist/ dist/specify_cli-*.whl
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Important:** `pip download` resolves platform-specific wheels (e.g., PyYAML includes native extensions). You must run this step on a machine with the **same OS and Python version** as the air-gapped target. If you need to support multiple platforms, repeat this step on each target OS (Linux, macOS, Windows) and Python version.
|
|
||||||
|
|
||||||
**Step 2: Transfer the `dist/` directory to the air-gapped machine**
|
|
||||||
|
|
||||||
Copy the entire `dist/` directory (which contains the `specify-cli` wheel and all dependency wheels) to the target machine via USB, network share, or other approved transfer method.
|
|
||||||
|
|
||||||
**Step 3: Install on the air-gapped machine**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install --no-index --find-links=./dist specify-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 4: Initialize a project (no network required)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Initialize a project — no GitHub access needed
|
|
||||||
specify init my-project --ai claude --offline
|
|
||||||
```
|
|
||||||
|
|
||||||
The `--offline` flag tells the CLI to use the templates, commands, and scripts bundled inside the wheel instead of downloading from GitHub.
|
|
||||||
|
|
||||||
> **Deprecation notice:** Starting with v0.6.0, `specify init` will use bundled assets by default and the `--offline` flag will be removed. The GitHub download path will be retired because bundled assets eliminate the need for network access, avoid proxy/firewall issues, and guarantee that templates always match the installed CLI version. No action will be needed — `specify init` will simply work without network access out of the box.
|
|
||||||
|
|
||||||
> **Note:** Python 3.11+ is required.
|
|
||||||
|
|
||||||
> **Windows note:** Offline scaffolding requires PowerShell 7+ (`pwsh`), not Windows PowerShell 5.x (`powershell.exe`). Install from https://aka.ms/powershell.
|
|
||||||
|
|
||||||
### Git Credential Manager on Linux
|
### Git Credential Manager on Linux
|
||||||
|
|
||||||
If you're having issues with Git authentication on Linux, you can install Git Credential Manager:
|
If you're having issues with Git authentication on Linux, you can install Git Credential Manager:
|
||||||
|
|||||||
@@ -173,6 +173,6 @@ Finally, implement the solution:
|
|||||||
|
|
||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
|
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
|
||||||
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
|
- Check out [more examples](../templates) in the repository
|
||||||
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
|
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
| What to Upgrade | Command | When to Use |
|
| What to Upgrade | Command | When to Use |
|
||||||
|----------------|---------|-------------|
|
|----------------|---------|-------------|
|
||||||
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | Get latest CLI features without touching project files |
|
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git` | Get latest CLI features without touching project files |
|
||||||
| **Project Files** | `specify init --here --force --ai <your-agent>` | Update slash commands, templates, and scripts in your project |
|
| **Project Files** | `specify init --here --force --ai <your-agent>` | Update slash commands, templates, and scripts in your project |
|
||||||
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
|
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
|
||||||
|
|
||||||
@@ -20,18 +20,16 @@ The CLI tool (`specify`) is separate from your project files. Upgrade it to get
|
|||||||
|
|
||||||
### If you installed with `uv tool install`
|
### If you installed with `uv tool install`
|
||||||
|
|
||||||
Upgrade to a specific release (check [Releases](https://github.com/github/spec-kit/releases) for the latest tag):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z
|
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
|
||||||
```
|
```
|
||||||
|
|
||||||
### If you use one-shot `uvx` commands
|
### If you use one-shot `uvx` commands
|
||||||
|
|
||||||
Specify the desired release tag:
|
No upgrade needed—`uvx` always fetches the latest version. Just run your commands as normal:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --ai copilot
|
uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai copilot
|
||||||
```
|
```
|
||||||
|
|
||||||
### Verify the upgrade
|
### Verify the upgrade
|
||||||
@@ -291,9 +289,8 @@ This tells Spec Kit which feature directory to use when creating specs, plans, a
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
ls -la .claude/commands/ # Claude Code
|
ls -la .claude/commands/ # Claude Code
|
||||||
ls -la .gemini/commands/ # Gemini
|
ls -la .gemini/commands/ # Gemini
|
||||||
ls -la .cursor/commands/ # Cursor
|
ls -la .cursor/commands/ # Cursor
|
||||||
ls -la .pi/prompts/ # Pi Coding Agent
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Check agent-specific setup:**
|
3. **Check agent-specific setup:**
|
||||||
@@ -401,7 +398,7 @@ The `specify` CLI tool is used for:
|
|||||||
- **Upgrades:** `specify init --here --force` to update templates and commands
|
- **Upgrades:** `specify init --here --force` to update templates and commands
|
||||||
- **Diagnostics:** `specify check` to verify tool installation
|
- **Diagnostics:** `specify check` to verify tool installation
|
||||||
|
|
||||||
Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/speckit.plan`, etc.) are **permanently installed** in your project's agent folder (`.claude/`, `.github/prompts/`, `.pi/prompts/`, etc.). Your AI assistant reads these command files directly—no need to run `specify` again.
|
Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/speckit.plan`, etc.) are **permanently installed** in your project's agent folder (`.claude/`, `.github/prompts/`, etc.). Your AI assistant reads these command files directly—no need to run `specify` again.
|
||||||
|
|
||||||
**If your agent isn't recognizing slash commands:**
|
**If your agent isn't recognizing slash commands:**
|
||||||
|
|
||||||
@@ -413,9 +410,6 @@ Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/s
|
|||||||
|
|
||||||
# For Claude
|
# For Claude
|
||||||
ls -la .claude/commands/
|
ls -la .claude/commands/
|
||||||
|
|
||||||
# For Pi
|
|
||||||
ls -la .pi/prompts/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Restart your IDE/editor completely** (not just reload window)
|
2. **Restart your IDE/editor completely** (not just reload window)
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ provides:
|
|||||||
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
|
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
|
||||||
file: string # Required, relative path to command file
|
file: string # Required, relative path to command file
|
||||||
description: string # Required
|
description: string # Required
|
||||||
aliases: [string] # Optional, same pattern as name; namespace must match extension.id and must not shadow core or installed extension commands
|
aliases: [string] # Optional, array of alternate names
|
||||||
|
|
||||||
config: # Optional, array of config files
|
config: # Optional, array of config files
|
||||||
- name: string # Config file name
|
- name: string # Config file name
|
||||||
@@ -53,7 +53,7 @@ provides:
|
|||||||
required: boolean # Default: false
|
required: boolean # Default: false
|
||||||
|
|
||||||
hooks: # Optional, event hooks
|
hooks: # Optional, event hooks
|
||||||
event_name: # e.g., "after_specify", "after_plan", "after_tasks", "after_implement"
|
event_name: # e.g., "after_tasks", "after_implement"
|
||||||
command: string # Command to execute
|
command: string # Command to execute
|
||||||
optional: boolean # Default: true
|
optional: boolean # Default: true
|
||||||
prompt: string # Prompt text for optional hooks
|
prompt: string # Prompt text for optional hooks
|
||||||
@@ -108,7 +108,7 @@ defaults: # Optional, default configuration values
|
|||||||
#### `hooks`
|
#### `hooks`
|
||||||
|
|
||||||
- **Type**: object
|
- **Type**: object
|
||||||
- **Keys**: Event names (e.g., `after_specify`, `after_plan`, `after_tasks`, `after_implement`, `before_commit`)
|
- **Keys**: Event names (e.g., `after_tasks`, `after_implement`, `before_commit`)
|
||||||
- **Description**: Hooks that execute at lifecycle events
|
- **Description**: Hooks that execute at lifecycle events
|
||||||
- **Events**: Defined by core spec-kit commands
|
- **Events**: Defined by core spec-kit commands
|
||||||
|
|
||||||
@@ -243,34 +243,6 @@ manager.check_compatibility(
|
|||||||
) # Raises: CompatibilityError if incompatible
|
) # Raises: CompatibilityError if incompatible
|
||||||
```
|
```
|
||||||
|
|
||||||
### CatalogEntry
|
|
||||||
|
|
||||||
**Module**: `specify_cli.extensions`
|
|
||||||
|
|
||||||
Represents a single catalog in the active catalog stack.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from specify_cli.extensions import CatalogEntry
|
|
||||||
|
|
||||||
entry = CatalogEntry(
|
|
||||||
url="https://example.com/catalog.json",
|
|
||||||
name="default",
|
|
||||||
priority=1,
|
|
||||||
install_allowed=True,
|
|
||||||
description="Built-in catalog of installable extensions",
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Fields**:
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `url` | `str` | Catalog URL (must use HTTPS, or HTTP for localhost) |
|
|
||||||
| `name` | `str` | Human-readable catalog name |
|
|
||||||
| `priority` | `int` | Sort order (lower = higher priority, wins on conflicts) |
|
|
||||||
| `install_allowed` | `bool` | Whether extensions from this catalog can be installed |
|
|
||||||
| `description` | `str` | Optional human-readable description of the catalog (default: empty) |
|
|
||||||
|
|
||||||
### ExtensionCatalog
|
### ExtensionCatalog
|
||||||
|
|
||||||
**Module**: `specify_cli.extensions`
|
**Module**: `specify_cli.extensions`
|
||||||
@@ -281,67 +253,30 @@ from specify_cli.extensions import ExtensionCatalog
|
|||||||
catalog = ExtensionCatalog(project_root)
|
catalog = ExtensionCatalog(project_root)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Class attributes**:
|
|
||||||
|
|
||||||
```python
|
|
||||||
ExtensionCatalog.DEFAULT_CATALOG_URL # default catalog URL
|
|
||||||
ExtensionCatalog.COMMUNITY_CATALOG_URL # community catalog URL
|
|
||||||
```
|
|
||||||
|
|
||||||
**Methods**:
|
**Methods**:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Get the ordered list of active catalogs
|
# Fetch catalog
|
||||||
entries = catalog.get_active_catalogs() # List[CatalogEntry]
|
|
||||||
|
|
||||||
# Fetch catalog (primary catalog, backward compat)
|
|
||||||
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
|
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
|
||||||
|
|
||||||
# Search extensions across all active catalogs
|
# Search extensions
|
||||||
# Each result includes _catalog_name and _install_allowed
|
|
||||||
results = catalog.search(
|
results = catalog.search(
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
tag: Optional[str] = None,
|
tag: Optional[str] = None,
|
||||||
author: Optional[str] = None,
|
author: Optional[str] = None,
|
||||||
verified_only: bool = False
|
verified_only: bool = False
|
||||||
) # Returns: List[Dict] — each dict includes _catalog_name, _install_allowed
|
) # Returns: List[Dict]
|
||||||
|
|
||||||
# Get extension info (searches all active catalogs)
|
# Get extension info
|
||||||
# Returns None if not found; includes _catalog_name and _install_allowed
|
|
||||||
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
|
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
|
||||||
|
|
||||||
# Check cache validity (primary catalog)
|
# Check cache validity
|
||||||
is_valid = catalog.is_cache_valid() # bool
|
is_valid = catalog.is_cache_valid() # bool
|
||||||
|
|
||||||
# Clear all catalog caches
|
# Clear cache
|
||||||
catalog.clear_cache()
|
catalog.clear_cache()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Result annotation fields**:
|
|
||||||
|
|
||||||
Each extension dict returned by `search()` and `get_extension_info()` includes:
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `_catalog_name` | `str` | Name of the source catalog |
|
|
||||||
| `_install_allowed` | `bool` | Whether installation is allowed from this catalog |
|
|
||||||
|
|
||||||
**Catalog config file** (`.specify/extension-catalogs.yml`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
catalogs:
|
|
||||||
- name: "default"
|
|
||||||
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
|
||||||
priority: 1
|
|
||||||
install_allowed: true
|
|
||||||
description: "Built-in catalog of installable extensions"
|
|
||||||
- name: "community"
|
|
||||||
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
|
||||||
priority: 2
|
|
||||||
install_allowed: false
|
|
||||||
description: "Community-contributed extensions (discovery only)"
|
|
||||||
```
|
|
||||||
|
|
||||||
### HookExecutor
|
### HookExecutor
|
||||||
|
|
||||||
**Module**: `specify_cli.extensions`
|
**Module**: `specify_cli.extensions`
|
||||||
@@ -551,16 +486,10 @@ hooks:
|
|||||||
|
|
||||||
Standard events (defined by core):
|
Standard events (defined by core):
|
||||||
|
|
||||||
- `before_specify` - Before specification generation
|
|
||||||
- `after_specify` - After specification generation
|
|
||||||
- `before_plan` - Before implementation planning
|
|
||||||
- `after_plan` - After implementation planning
|
|
||||||
- `before_tasks` - Before task generation
|
|
||||||
- `after_tasks` - After task generation
|
- `after_tasks` - After task generation
|
||||||
- `before_implement` - Before implementation
|
|
||||||
- `after_implement` - After implementation
|
- `after_implement` - After implementation
|
||||||
- `before_commit` - Before git commit *(planned - not yet wired into core templates)*
|
- `before_commit` - Before git commit
|
||||||
- `after_commit` - After git commit *(planned - not yet wired into core templates)*
|
- `after_commit` - After git commit
|
||||||
|
|
||||||
### Hook Configuration
|
### Hook Configuration
|
||||||
|
|
||||||
@@ -614,39 +543,6 @@ EXECUTE_COMMAND: {command}
|
|||||||
|
|
||||||
**Output**: List of installed extensions with metadata
|
**Output**: List of installed extensions with metadata
|
||||||
|
|
||||||
### extension catalog list
|
|
||||||
|
|
||||||
**Usage**: `specify extension catalog list`
|
|
||||||
|
|
||||||
Lists all active catalogs in the current catalog stack, showing name, description, URL, priority, and `install_allowed` status.
|
|
||||||
|
|
||||||
### extension catalog add
|
|
||||||
|
|
||||||
**Usage**: `specify extension catalog add URL [OPTIONS]`
|
|
||||||
|
|
||||||
**Options**:
|
|
||||||
|
|
||||||
- `--name NAME` - Catalog name (required)
|
|
||||||
- `--priority INT` - Priority (lower = higher priority, default: 10)
|
|
||||||
- `--install-allowed / --no-install-allowed` - Allow installs from this catalog (default: false)
|
|
||||||
- `--description TEXT` - Optional description of the catalog
|
|
||||||
|
|
||||||
**Arguments**:
|
|
||||||
|
|
||||||
- `URL` - Catalog URL (must use HTTPS)
|
|
||||||
|
|
||||||
Adds a catalog entry to `.specify/extension-catalogs.yml`.
|
|
||||||
|
|
||||||
### extension catalog remove
|
|
||||||
|
|
||||||
**Usage**: `specify extension catalog remove NAME`
|
|
||||||
|
|
||||||
**Arguments**:
|
|
||||||
|
|
||||||
- `NAME` - Catalog name to remove
|
|
||||||
|
|
||||||
Removes a catalog entry from `.specify/extension-catalogs.yml`.
|
|
||||||
|
|
||||||
### extension add
|
### extension add
|
||||||
|
|
||||||
**Usage**: `specify extension add EXTENSION [OPTIONS]`
|
**Usage**: `specify extension add EXTENSION [OPTIONS]`
|
||||||
@@ -655,13 +551,13 @@ Removes a catalog entry from `.specify/extension-catalogs.yml`.
|
|||||||
|
|
||||||
- `--from URL` - Install from custom URL
|
- `--from URL` - Install from custom URL
|
||||||
- `--dev PATH` - Install from local directory
|
- `--dev PATH` - Install from local directory
|
||||||
|
- `--version VERSION` - Install specific version
|
||||||
|
- `--no-register` - Skip command registration
|
||||||
|
|
||||||
**Arguments**:
|
**Arguments**:
|
||||||
|
|
||||||
- `EXTENSION` - Extension name or URL
|
- `EXTENSION` - Extension name or URL
|
||||||
|
|
||||||
**Note**: Extensions from catalogs with `install_allowed: false` cannot be installed via this command.
|
|
||||||
|
|
||||||
### extension remove
|
### extension remove
|
||||||
|
|
||||||
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
|
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
|
||||||
@@ -679,8 +575,6 @@ Removes a catalog entry from `.specify/extension-catalogs.yml`.
|
|||||||
|
|
||||||
**Usage**: `specify extension search [QUERY] [OPTIONS]`
|
**Usage**: `specify extension search [QUERY] [OPTIONS]`
|
||||||
|
|
||||||
Searches all active catalogs simultaneously. Results include source catalog name and install_allowed status.
|
|
||||||
|
|
||||||
**Options**:
|
**Options**:
|
||||||
|
|
||||||
- `--tag TAG` - Filter by tag
|
- `--tag TAG` - Filter by tag
|
||||||
@@ -695,8 +589,6 @@ Searches all active catalogs simultaneously. Results include source catalog name
|
|||||||
|
|
||||||
**Usage**: `specify extension info EXTENSION`
|
**Usage**: `specify extension info EXTENSION`
|
||||||
|
|
||||||
Shows source catalog and install_allowed status.
|
|
||||||
|
|
||||||
**Arguments**:
|
**Arguments**:
|
||||||
|
|
||||||
- `EXTENSION` - Extension ID
|
- `EXTENSION` - Extension ID
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ provides:
|
|||||||
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
|
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
|
||||||
file: "commands/hello.md"
|
file: "commands/hello.md"
|
||||||
description: "Say hello"
|
description: "Say hello"
|
||||||
aliases: ["speckit.my-ext.hi"] # Optional aliases, same pattern
|
aliases: ["speckit.hello"] # Optional aliases
|
||||||
|
|
||||||
config: # Optional: Config files
|
config: # Optional: Config files
|
||||||
- name: "my-ext-config.yml"
|
- name: "my-ext-config.yml"
|
||||||
@@ -186,7 +186,7 @@ What the extension provides.
|
|||||||
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
|
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
|
||||||
- `file`: Path to command file (relative to extension root)
|
- `file`: Path to command file (relative to extension root)
|
||||||
- `description`: Command description (optional)
|
- `description`: Command description (optional)
|
||||||
- `aliases`: Alternative command names (optional, array; each must match `speckit.{ext-id}.{command}`)
|
- `aliases`: Alternative command names (optional, array)
|
||||||
|
|
||||||
### Optional Fields
|
### Optional Fields
|
||||||
|
|
||||||
@@ -332,67 +332,6 @@ echo "$config"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Excluding Files with `.extensionignore`
|
|
||||||
|
|
||||||
Extension authors can create a `.extensionignore` file in the extension root to exclude files and folders from being copied when a user installs the extension with `specify extension add`. This is useful for keeping development-only files (tests, CI configs, docs source, etc.) out of the installed copy.
|
|
||||||
|
|
||||||
### Format
|
|
||||||
|
|
||||||
The file uses `.gitignore`-compatible patterns (one per line), powered by the [`pathspec`](https://pypi.org/project/pathspec/) library:
|
|
||||||
|
|
||||||
- Blank lines are ignored
|
|
||||||
- Lines starting with `#` are comments
|
|
||||||
- `*` matches anything **except** `/` (does not cross directory boundaries)
|
|
||||||
- `**` matches zero or more directories (e.g., `docs/**/*.draft.md`)
|
|
||||||
- `?` matches any single character except `/`
|
|
||||||
- A trailing `/` restricts a pattern to directories only
|
|
||||||
- Patterns containing `/` (other than a trailing slash) are anchored to the extension root
|
|
||||||
- Patterns without `/` match at any depth in the tree
|
|
||||||
- `!` negates a previously excluded pattern (re-includes a file)
|
|
||||||
- Backslashes in patterns are normalised to forward slashes for cross-platform compatibility
|
|
||||||
- The `.extensionignore` file itself is always excluded automatically
|
|
||||||
|
|
||||||
### Example
|
|
||||||
|
|
||||||
```gitignore
|
|
||||||
# .extensionignore
|
|
||||||
|
|
||||||
# Development files
|
|
||||||
tests/
|
|
||||||
.github/
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
dist/
|
|
||||||
|
|
||||||
# Documentation source (keep only the built README)
|
|
||||||
docs/
|
|
||||||
CONTRIBUTING.md
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pattern Matching
|
|
||||||
|
|
||||||
| Pattern | Matches | Does NOT match |
|
|
||||||
|---------|---------|----------------|
|
|
||||||
| `*.pyc` | Any `.pyc` file in any directory | — |
|
|
||||||
| `tests/` | The `tests` directory (and all its contents) | A file named `tests` |
|
|
||||||
| `docs/*.draft.md` | `docs/api.draft.md` (directly inside `docs/`) | `docs/sub/api.draft.md` (nested) |
|
|
||||||
| `.env` | The `.env` file at any level | — |
|
|
||||||
| `!README.md` | Re-includes `README.md` even if matched by an earlier pattern | — |
|
|
||||||
| `docs/**/*.draft.md` | `docs/api.draft.md`, `docs/sub/api.draft.md` | — |
|
|
||||||
|
|
||||||
### Unsupported Features
|
|
||||||
|
|
||||||
The following `.gitignore` features are **not applicable** in this context:
|
|
||||||
|
|
||||||
- **Multiple `.extensionignore` files**: Only a single file at the extension root is supported (`.gitignore` supports files in subdirectories)
|
|
||||||
- **`$GIT_DIR/info/exclude` and `core.excludesFile`**: These are Git-specific and have no equivalent here
|
|
||||||
- **Negation inside excluded directories**: Because file copying uses `shutil.copytree`, excluding a directory prevents recursion into it entirely. A negation pattern cannot re-include a file inside a directory that was itself excluded. For example, the combination `tests/` followed by `!tests/important.py` will **not** preserve `tests/important.py` — the `tests/` directory is skipped at the root level and its contents are never evaluated. To work around this, exclude the directory's contents individually instead of the directory itself (e.g., `tests/*.pyc` and `tests/.cache/` rather than `tests/`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Validation Rules
|
## Validation Rules
|
||||||
|
|
||||||
### Extension ID
|
### Extension ID
|
||||||
@@ -514,7 +453,7 @@ zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/
|
|||||||
Users install with:
|
Users install with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension add <extension-name> --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Option 3: Community Reference Catalog
|
### Option 3: Community Reference Catalog
|
||||||
@@ -523,7 +462,7 @@ Submit to the community catalog for public discovery:
|
|||||||
|
|
||||||
1. **Fork** spec-kit repository
|
1. **Fork** spec-kit repository
|
||||||
2. **Add entry** to `extensions/catalog.community.json`
|
2. **Add entry** to `extensions/catalog.community.json`
|
||||||
3. **Update** the Community Extensions table in `README.md` with your extension
|
3. **Update** `extensions/README.md` with your extension
|
||||||
4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md)
|
4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md)
|
||||||
5. **After merge**, your extension becomes available:
|
5. **After merge**, your extension becomes available:
|
||||||
- Users can browse `catalog.community.json` to discover your extension
|
- Users can browse `catalog.community.json` to discover your extension
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ Test that users can install from your release:
|
|||||||
specify extension add --dev /path/to/your-extension
|
specify extension add --dev /path/to/your-extension
|
||||||
|
|
||||||
# Test from GitHub archive
|
# Test from GitHub archive
|
||||||
specify extension add <extension-name> --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -204,27 +204,14 @@ Edit `extensions/catalog.community.json` and add your extension:
|
|||||||
- Use current timestamp for `created_at` and `updated_at`
|
- Use current timestamp for `created_at` and `updated_at`
|
||||||
- Update the top-level `updated_at` to current time
|
- Update the top-level `updated_at` to current time
|
||||||
|
|
||||||
### 3. Update Community Extensions Table
|
### 3. Update Extensions README
|
||||||
|
|
||||||
Add your extension to the Community Extensions table in the project root `README.md`:
|
Add your extension to the Available Extensions table in `extensions/README.md`:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
| Your Extension Name | Brief description of what it does | `<category>` | <effect> | [repo-name](https://github.com/your-org/spec-kit-your-extension) |
|
| Your Extension Name | Brief description of what it does | [repo-name](https://github.com/your-org/spec-kit-your-extension) |
|
||||||
```
|
```
|
||||||
|
|
||||||
**(Table) Category** — pick the one that best fits your extension:
|
|
||||||
|
|
||||||
- `docs` — reads, validates, or generates spec artifacts
|
|
||||||
- `code` — reviews, validates, or modifies source code
|
|
||||||
- `process` — orchestrates workflow across phases
|
|
||||||
- `integration` — syncs with external platforms
|
|
||||||
- `visibility` — reports on project health or progress
|
|
||||||
|
|
||||||
**Effect** — choose one:
|
|
||||||
|
|
||||||
- Read-only — produces reports without modifying files
|
|
||||||
- Read+Write — modifies files, creates artifacts, or updates specs
|
|
||||||
|
|
||||||
Insert your extension in alphabetical order in the table.
|
Insert your extension in alphabetical order in the table.
|
||||||
|
|
||||||
### 4. Submit Pull Request
|
### 4. Submit Pull Request
|
||||||
@@ -234,7 +221,7 @@ Insert your extension in alphabetical order in the table.
|
|||||||
git checkout -b add-your-extension
|
git checkout -b add-your-extension
|
||||||
|
|
||||||
# Commit your changes
|
# Commit your changes
|
||||||
git add extensions/catalog.community.json README.md
|
git add extensions/catalog.community.json extensions/README.md
|
||||||
git commit -m "Add your-extension to community catalog
|
git commit -m "Add your-extension to community catalog
|
||||||
|
|
||||||
- Extension ID: your-extension
|
- Extension ID: your-extension
|
||||||
@@ -273,7 +260,7 @@ Brief description of what your extension does.
|
|||||||
- [x] All commands working
|
- [x] All commands working
|
||||||
- [x] No security vulnerabilities
|
- [x] No security vulnerabilities
|
||||||
- [x] Added to extensions/catalog.community.json
|
- [x] Added to extensions/catalog.community.json
|
||||||
- [x] Added to Community Extensions table in README.md
|
- [x] Added to extensions/README.md Available Extensions table
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
Tested on:
|
Tested on:
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ vim .specify/extensions/jira/jira-config.yml
|
|||||||
|
|
||||||
## Finding Extensions
|
## Finding Extensions
|
||||||
|
|
||||||
`specify extension search` searches **all active catalogs** simultaneously, including the community catalog by default. Results are annotated with their source catalog and install status.
|
**Note**: By default, `specify extension search` uses your organization's catalog (`catalog.json`). If the catalog is empty, you won't see any results. See [Extension Catalogs](#extension-catalogs) to learn how to populate your catalog from the community reference catalog.
|
||||||
|
|
||||||
### Browse All Extensions
|
### Browse All Extensions
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ vim .specify/extensions/jira/jira-config.yml
|
|||||||
specify extension search
|
specify extension search
|
||||||
```
|
```
|
||||||
|
|
||||||
Shows all extensions across all active catalogs (default and community by default).
|
Shows all extensions in your organization's catalog.
|
||||||
|
|
||||||
### Search by Keyword
|
### Search by Keyword
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ This will:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From GitHub release
|
# From GitHub release
|
||||||
specify extension add <extension-name> --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install from Local Directory (Development)
|
### Install from Local Directory (Development)
|
||||||
@@ -187,21 +187,6 @@ Provided commands:
|
|||||||
Check: .specify/extensions/jira/
|
Check: .specify/extensions/jira/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Automatic Agent Skill Registration
|
|
||||||
|
|
||||||
If your project was initialized with `--ai-skills`, extension commands are **automatically registered as agent skills** during installation. This ensures that extensions are discoverable by agents that use the [agentskills.io](https://agentskills.io) skill specification.
|
|
||||||
|
|
||||||
```text
|
|
||||||
✓ Extension installed successfully!
|
|
||||||
|
|
||||||
Jira Integration (v1.0.0)
|
|
||||||
...
|
|
||||||
|
|
||||||
✓ 3 agent skill(s) auto-registered
|
|
||||||
```
|
|
||||||
|
|
||||||
When an extension is removed, its corresponding skills are also cleaned up automatically. Pre-existing skills that were manually customized are never overwritten.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Using Extensions
|
## Using Extensions
|
||||||
@@ -214,8 +199,8 @@ Extensions add commands that appear in your AI agent (Claude Code):
|
|||||||
# In Claude Code
|
# In Claude Code
|
||||||
> /speckit.jira.specstoissues
|
> /speckit.jira.specstoissues
|
||||||
|
|
||||||
# Or use a namespaced alias (if provided)
|
# Or use short alias (if provided)
|
||||||
> /speckit.jira.sync
|
> /speckit.specstoissues
|
||||||
```
|
```
|
||||||
|
|
||||||
### Extension Configuration
|
### Extension Configuration
|
||||||
@@ -402,9 +387,6 @@ settings:
|
|||||||
auto_execute_hooks: true
|
auto_execute_hooks: true
|
||||||
|
|
||||||
# Hook configuration
|
# Hook configuration
|
||||||
# Available events: before_specify, after_specify, before_plan, after_plan,
|
|
||||||
# before_tasks, after_tasks, before_implement, after_implement
|
|
||||||
# Planned (not yet wired into core templates): before_commit, after_commit
|
|
||||||
hooks:
|
hooks:
|
||||||
after_tasks:
|
after_tasks:
|
||||||
- extension: jira
|
- extension: jira
|
||||||
@@ -420,13 +402,13 @@ In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`),
|
|||||||
|
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `SPECKIT_CATALOG_URL` | Override the full catalog stack with a single URL (backward compat) | Built-in default stack |
|
| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog |
|
||||||
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
|
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
|
||||||
|
|
||||||
#### Example: Using a custom catalog for testing
|
#### Example: Using a custom catalog for testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Point to a local or alternative catalog (replaces the full stack)
|
# Point to a local or alternative catalog
|
||||||
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
|
||||||
|
|
||||||
# Or use a staging catalog
|
# Or use a staging catalog
|
||||||
@@ -437,96 +419,13 @@ export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
|
|||||||
|
|
||||||
## Extension Catalogs
|
## Extension Catalogs
|
||||||
|
|
||||||
Spec Kit uses a **catalog stack** — an ordered list of catalogs searched simultaneously. By default, two catalogs are active:
|
For information about how Spec Kit's dual-catalog system works (`catalog.json` vs `catalog.community.json`), see the main [Extensions README](README.md#extension-catalogs).
|
||||||
|
|
||||||
| Priority | Catalog | Install Allowed | Purpose |
|
|
||||||
|----------|---------|-----------------|---------|
|
|
||||||
| 1 | `catalog.json` (default) | ✅ Yes | Curated extensions available for installation |
|
|
||||||
| 2 | `catalog.community.json` (community) | ❌ No (discovery only) | Browse community extensions |
|
|
||||||
|
|
||||||
### Listing Active Catalogs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension catalog list
|
|
||||||
```
|
|
||||||
|
|
||||||
### Managing Catalogs via CLI
|
|
||||||
|
|
||||||
You can view the main catalog management commands using `--help`:
|
|
||||||
|
|
||||||
```text
|
|
||||||
specify extension catalog --help
|
|
||||||
|
|
||||||
Usage: specify extension catalog [OPTIONS] COMMAND [ARGS]...
|
|
||||||
|
|
||||||
Manage extension catalogs
|
|
||||||
╭─ Options ────────────────────────────────────────────────────────────────────────╮
|
|
||||||
│ --help Show this message and exit. │
|
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────╯
|
|
||||||
╭─ Commands ───────────────────────────────────────────────────────────────────────╮
|
|
||||||
│ list List all active extension catalogs. │
|
|
||||||
│ add Add a catalog to .specify/extension-catalogs.yml. │
|
|
||||||
│ remove Remove a catalog from .specify/extension-catalogs.yml. │
|
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────╯
|
|
||||||
```
|
|
||||||
|
|
||||||
### Adding a Catalog (Project-scoped)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Add an internal catalog that allows installs
|
|
||||||
specify extension catalog add \
|
|
||||||
--name "internal" \
|
|
||||||
--priority 2 \
|
|
||||||
--install-allowed \
|
|
||||||
https://internal.company.com/spec-kit/catalog.json
|
|
||||||
|
|
||||||
# Add a discovery-only catalog
|
|
||||||
specify extension catalog add \
|
|
||||||
--name "partner" \
|
|
||||||
--priority 5 \
|
|
||||||
https://partner.example.com/spec-kit/catalog.json
|
|
||||||
```
|
|
||||||
|
|
||||||
This creates or updates `.specify/extension-catalogs.yml`.
|
|
||||||
|
|
||||||
### Removing a Catalog
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension catalog remove internal
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Config File
|
|
||||||
|
|
||||||
You can also edit `.specify/extension-catalogs.yml` directly:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
catalogs:
|
|
||||||
- name: "default"
|
|
||||||
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
|
||||||
priority: 1
|
|
||||||
install_allowed: true
|
|
||||||
description: "Built-in catalog of installable extensions"
|
|
||||||
|
|
||||||
- name: "internal"
|
|
||||||
url: "https://internal.company.com/spec-kit/catalog.json"
|
|
||||||
priority: 2
|
|
||||||
install_allowed: true
|
|
||||||
description: "Internal company extensions"
|
|
||||||
|
|
||||||
- name: "community"
|
|
||||||
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
|
||||||
priority: 3
|
|
||||||
install_allowed: false
|
|
||||||
description: "Community-contributed extensions (discovery only)"
|
|
||||||
```
|
|
||||||
|
|
||||||
A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. Project-level config takes full precedence when it contains one or more catalog entries. An empty `catalogs: []` list falls back to built-in defaults.
|
|
||||||
|
|
||||||
## Organization Catalog Customization
|
## Organization Catalog Customization
|
||||||
|
|
||||||
### Why Customize Your Catalog
|
### Why Customize Your Catalog
|
||||||
|
|
||||||
Organizations customize their catalogs to:
|
Organizations customize their `catalog.json` to:
|
||||||
|
|
||||||
- **Control available extensions** - Curate which extensions your team can install
|
- **Control available extensions** - Curate which extensions your team can install
|
||||||
- **Host private extensions** - Internal tools that shouldn't be public
|
- **Host private extensions** - Internal tools that shouldn't be public
|
||||||
@@ -604,40 +503,24 @@ Options for hosting your catalog:
|
|||||||
|
|
||||||
#### 3. Configure Your Environment
|
#### 3. Configure Your Environment
|
||||||
|
|
||||||
##### Option A: Catalog stack config file (recommended)
|
##### Option A: Environment variable (recommended for CI/CD)
|
||||||
|
|
||||||
Add to `.specify/extension-catalogs.yml` in your project:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
catalogs:
|
|
||||||
- name: "my-org"
|
|
||||||
url: "https://your-org.com/spec-kit/catalog.json"
|
|
||||||
priority: 1
|
|
||||||
install_allowed: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the CLI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension catalog add \
|
|
||||||
--name "my-org" \
|
|
||||||
--install-allowed \
|
|
||||||
https://your-org.com/spec-kit/catalog.json
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Option B: Environment variable (recommended for CI/CD, single-catalog)
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# In ~/.bashrc, ~/.zshrc, or CI pipeline
|
# In ~/.bashrc, ~/.zshrc, or CI pipeline
|
||||||
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
|
export SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Option B: Per-project configuration
|
||||||
|
|
||||||
|
Create `.env` or set in your shell before running spec-kit commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SPECKIT_CATALOG_URL="https://your-org.com/spec-kit/catalog.json" specify extension search
|
||||||
|
```
|
||||||
|
|
||||||
#### 4. Verify Configuration
|
#### 4. Verify Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List active catalogs
|
|
||||||
specify extension catalog list
|
|
||||||
|
|
||||||
# Search should now show your catalog's extensions
|
# Search should now show your catalog's extensions
|
||||||
specify extension search
|
specify extension search
|
||||||
|
|
||||||
@@ -737,7 +620,7 @@ You can still install extensions not in your catalog using `--from`:
|
|||||||
specify extension add jira
|
specify extension add jira
|
||||||
|
|
||||||
# Direct URL (bypasses catalog)
|
# Direct URL (bypasses catalog)
|
||||||
specify extension add <extension-name> --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
|
specify extension add --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
|
||||||
|
|
||||||
# Local development
|
# Local development
|
||||||
specify extension add --dev /path/to/extension
|
specify extension add --dev /path/to/extension
|
||||||
@@ -807,7 +690,7 @@ specify extension add --dev /path/to/extension
|
|||||||
2. Install older version of extension:
|
2. Install older version of extension:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension add <extension-name> --from https://github.com/org/ext/archive/v1.0.0.zip
|
specify extension add --from https://github.com/org/ext/archive/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### MCP Tool Not Available
|
### MCP Tool Not Available
|
||||||
|
|||||||
@@ -24,9 +24,6 @@ specify extension search # Now uses your organization's catalog instead of the
|
|||||||
|
|
||||||
### Community Reference Catalog (`catalog.community.json`)
|
### Community Reference Catalog (`catalog.community.json`)
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. Review extension source code before installation and use at your own discretion.
|
|
||||||
|
|
||||||
- **Purpose**: Browse available community-contributed extensions
|
- **Purpose**: Browse available community-contributed extensions
|
||||||
- **Status**: Active - contains extensions submitted by the community
|
- **Status**: Active - contains extensions submitted by the community
|
||||||
- **Location**: `extensions/catalog.community.json`
|
- **Location**: `extensions/catalog.community.json`
|
||||||
@@ -62,7 +59,7 @@ Populate your `catalog.json` with approved extensions:
|
|||||||
Skip catalog curation - team members install directly using URLs:
|
Skip catalog curation - team members install directly using URLs:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
specify extension add <extension-name> --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
**Benefits**: Quick for one-off testing or private extensions
|
**Benefits**: Quick for one-off testing or private extensions
|
||||||
@@ -71,14 +68,15 @@ specify extension add <extension-name> --from https://github.com/org/spec-kit-ex
|
|||||||
|
|
||||||
## Available Community Extensions
|
## Available Community Extensions
|
||||||
|
|
||||||
> [!NOTE]
|
The following community-contributed extensions are available in [`catalog.community.json`](catalog.community.json):
|
||||||
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
|
|
||||||
|
|
||||||
🔍 **Browse and search community extensions on the [Community Extensions website](https://speckit-community.github.io/extensions/).**
|
| Extension | Purpose | URL |
|
||||||
|
|-----------|---------|-----|
|
||||||
See the [Community Extensions](../README.md#-community-extensions) section in the main README for the full list of available community-contributed extensions.
|
| Cleanup Extension | Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues | [spec-kit-cleanup](https://github.com/dsrednicki/spec-kit-cleanup) |
|
||||||
|
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
|
||||||
For the raw catalog data, see [`catalog.community.json`](catalog.community.json).
|
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||||
|
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||||
|
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
||||||
|
|
||||||
|
|
||||||
## Adding Your Extension
|
## Adding Your Extension
|
||||||
@@ -116,7 +114,7 @@ specify extension search # See what's in your catalog
|
|||||||
specify extension add <extension-name> # Install by name
|
specify extension add <extension-name> # Install by name
|
||||||
|
|
||||||
# Direct from URL (bypasses catalog)
|
# Direct from URL (bypasses catalog)
|
||||||
specify extension add <extension-name> --from https://github.com/<org>/<repo>/archive/refs/tags/<version>.zip
|
specify extension add --from https://github.com/<org>/<repo>/archive/refs/tags/<version>.zip
|
||||||
|
|
||||||
# List installed extensions
|
# List installed extensions
|
||||||
specify extension list
|
specify extension list
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# RFC: Spec Kit Extension System
|
# RFC: Spec Kit Extension System
|
||||||
|
|
||||||
**Status**: Implemented
|
**Status**: Draft
|
||||||
**Author**: Stats Perform Engineering
|
**Author**: Stats Perform Engineering
|
||||||
**Created**: 2026-01-28
|
**Created**: 2026-01-28
|
||||||
**Updated**: 2026-03-11
|
**Updated**: 2026-01-28
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -24,9 +24,8 @@
|
|||||||
13. [Security Considerations](#security-considerations)
|
13. [Security Considerations](#security-considerations)
|
||||||
14. [Migration Strategy](#migration-strategy)
|
14. [Migration Strategy](#migration-strategy)
|
||||||
15. [Implementation Phases](#implementation-phases)
|
15. [Implementation Phases](#implementation-phases)
|
||||||
16. [Resolved Questions](#resolved-questions)
|
16. [Open Questions](#open-questions)
|
||||||
17. [Open Questions (Remaining)](#open-questions-remaining)
|
17. [Appendices](#appendices)
|
||||||
18. [Appendices](#appendices)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -223,7 +222,7 @@ provides:
|
|||||||
- name: "speckit.jira.specstoissues"
|
- name: "speckit.jira.specstoissues"
|
||||||
file: "commands/specstoissues.md"
|
file: "commands/specstoissues.md"
|
||||||
description: "Create Jira hierarchy from spec and tasks"
|
description: "Create Jira hierarchy from spec and tasks"
|
||||||
aliases: ["speckit.jira.sync"] # Alternate names
|
aliases: ["speckit.specstoissues"] # Alternate names
|
||||||
|
|
||||||
- name: "speckit.jira.discover-fields"
|
- name: "speckit.jira.discover-fields"
|
||||||
file: "commands/discover-fields.md"
|
file: "commands/discover-fields.md"
|
||||||
@@ -359,15 +358,12 @@ specify extension add jira
|
|||||||
"installed_at": "2026-01-28T14:30:00Z",
|
"installed_at": "2026-01-28T14:30:00Z",
|
||||||
"source": "catalog",
|
"source": "catalog",
|
||||||
"manifest_hash": "sha256:abc123...",
|
"manifest_hash": "sha256:abc123...",
|
||||||
"enabled": true,
|
"enabled": true
|
||||||
"priority": 10
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Priority Field**: Extensions are ordered by `priority` (lower = higher precedence). Default is 10. Used for template resolution when multiple extensions provide the same template.
|
|
||||||
|
|
||||||
### 3. Configuration
|
### 3. Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -872,7 +868,7 @@ Spec Kit uses two catalog files with different purposes:
|
|||||||
|
|
||||||
- **Purpose**: Organization's curated catalog of approved extensions
|
- **Purpose**: Organization's curated catalog of approved extensions
|
||||||
- **Default State**: Empty by design - users populate with extensions they trust
|
- **Default State**: Empty by design - users populate with extensions they trust
|
||||||
- **Usage**: Primary catalog (priority 1, `install_allowed: true`) in the default stack
|
- **Usage**: Default catalog used by `specify extension` CLI commands
|
||||||
- **Control**: Organizations maintain their own fork/version for their teams
|
- **Control**: Organizations maintain their own fork/version for their teams
|
||||||
|
|
||||||
#### Community Reference Catalog (`catalog.community.json`)
|
#### Community Reference Catalog (`catalog.community.json`)
|
||||||
@@ -883,16 +879,16 @@ Spec Kit uses two catalog files with different purposes:
|
|||||||
- **Verification**: Community extensions may have `verified: false` initially
|
- **Verification**: Community extensions may have `verified: false` initially
|
||||||
- **Status**: Active - open for community contributions
|
- **Status**: Active - open for community contributions
|
||||||
- **Submission**: Via Pull Request following the Extension Publishing Guide
|
- **Submission**: Via Pull Request following the Extension Publishing Guide
|
||||||
- **Usage**: Secondary catalog (priority 2, `install_allowed: false`) in the default stack — discovery only
|
- **Usage**: Browse to discover extensions, then copy to your `catalog.json`
|
||||||
|
|
||||||
**How It Works (default stack):**
|
**How It Works:**
|
||||||
|
|
||||||
1. **Discover**: `specify extension search` searches both catalogs — community extensions appear automatically
|
1. **Discover**: Browse `catalog.community.json` to find available extensions
|
||||||
2. **Review**: Evaluate community extensions for security, quality, and organizational fit
|
2. **Review**: Evaluate extensions for security, quality, and organizational fit
|
||||||
3. **Curate**: Copy approved entries from community catalog to your `catalog.json`, or add to `.specify/extension-catalogs.yml` with `install_allowed: true`
|
3. **Curate**: Copy approved extension entries from community catalog to your `catalog.json`
|
||||||
4. **Install**: Use `specify extension add <name>` — only allowed from `install_allowed: true` catalogs
|
4. **Install**: Use `specify extension add <name>` (pulls from your curated catalog)
|
||||||
|
|
||||||
This approach gives organizations full control over which extensions can be installed while still providing community discoverability out of the box.
|
This approach gives organizations full control over which extensions are available to their teams while maintaining a shared community resource for discovery.
|
||||||
|
|
||||||
### Catalog Format
|
### Catalog Format
|
||||||
|
|
||||||
@@ -965,92 +961,30 @@ specify extension info jira
|
|||||||
|
|
||||||
### Custom Catalogs
|
### Custom Catalogs
|
||||||
|
|
||||||
Spec Kit supports a **catalog stack** — an ordered list of catalogs that the CLI merges and searches across. This allows organizations to maintain their own org-approved extensions alongside an internal catalog and community discovery, all at once.
|
**⚠️ FUTURE FEATURE - NOT YET IMPLEMENTED**
|
||||||
|
|
||||||
#### Catalog Stack Resolution
|
The following catalog management commands are proposed design concepts but are not yet available in the current implementation:
|
||||||
|
|
||||||
The active catalog stack is resolved in this order (first match wins):
|
|
||||||
|
|
||||||
1. **`SPECKIT_CATALOG_URL` environment variable** — single catalog replacing all defaults (backward compat)
|
|
||||||
2. **Project-level `.specify/extension-catalogs.yml`** — full control for the project
|
|
||||||
3. **User-level `~/.specify/extension-catalogs.yml`** — personal defaults
|
|
||||||
4. **Built-in default stack** — `catalog.json` (install_allowed: true) + `catalog.community.json` (install_allowed: false)
|
|
||||||
|
|
||||||
#### Default Built-in Stack
|
|
||||||
|
|
||||||
When no config file exists, the CLI uses:
|
|
||||||
|
|
||||||
| Priority | Catalog | install_allowed | Purpose |
|
|
||||||
|----------|---------|-----------------|---------|
|
|
||||||
| 1 | `catalog.json` (default) | `true` | Curated extensions available for installation |
|
|
||||||
| 2 | `catalog.community.json` (community) | `false` | Discovery only — browse but not install |
|
|
||||||
|
|
||||||
This means `specify extension search` surfaces community extensions out of the box, while `specify extension add` is still restricted to entries from catalogs with `install_allowed: true`.
|
|
||||||
|
|
||||||
#### `.specify/extension-catalogs.yml` Config File
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
catalogs:
|
|
||||||
- name: "default"
|
|
||||||
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json"
|
|
||||||
priority: 1 # Highest — only approved entries can be installed
|
|
||||||
install_allowed: true
|
|
||||||
description: "Built-in catalog of installable extensions"
|
|
||||||
|
|
||||||
- name: "internal"
|
|
||||||
url: "https://internal.company.com/spec-kit/catalog.json"
|
|
||||||
priority: 2
|
|
||||||
install_allowed: true
|
|
||||||
description: "Internal company extensions"
|
|
||||||
|
|
||||||
- name: "community"
|
|
||||||
url: "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json"
|
|
||||||
priority: 3 # Lowest — discovery only, not installable
|
|
||||||
install_allowed: false
|
|
||||||
description: "Community-contributed extensions (discovery only)"
|
|
||||||
```
|
|
||||||
|
|
||||||
A user-level equivalent lives at `~/.specify/extension-catalogs.yml`. When a project-level config is present with one or more catalog entries, it takes full control and the built-in defaults are not applied. An empty `catalogs: []` list is treated the same as no config file, falling back to defaults.
|
|
||||||
|
|
||||||
#### Catalog CLI Commands
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List active catalogs with name, URL, priority, and install_allowed
|
# Add custom catalog (FUTURE - NOT AVAILABLE)
|
||||||
specify extension catalog list
|
specify extension add-catalog https://internal.company.com/spec-kit/catalog.json
|
||||||
|
|
||||||
# Add a catalog (project-scoped)
|
# Set as default (FUTURE - NOT AVAILABLE)
|
||||||
specify extension catalog add --name "internal" --install-allowed \
|
specify extension set-catalog --default https://internal.company.com/spec-kit/catalog.json
|
||||||
https://internal.company.com/spec-kit/catalog.json
|
|
||||||
|
|
||||||
# Add a discovery-only catalog
|
# List catalogs (FUTURE - NOT AVAILABLE)
|
||||||
specify extension catalog add --name "community" \
|
specify extension catalogs
|
||||||
https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json
|
|
||||||
|
|
||||||
# Remove a catalog
|
|
||||||
specify extension catalog remove internal
|
|
||||||
|
|
||||||
# Show which catalog an extension came from
|
|
||||||
specify extension info jira
|
|
||||||
# → Source catalog: default
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Merge Conflict Resolution
|
**Proposed catalog priority** (future design):
|
||||||
|
|
||||||
When the same extension `id` appears in multiple catalogs, the higher-priority (lower priority number) catalog wins. Extensions from lower-priority catalogs with the same `id` are ignored.
|
1. Project-specific catalog (`.specify/extension-catalogs.yml`) - *not implemented*
|
||||||
|
2. User-level catalog (`~/.specify/extension-catalogs.yml`) - *not implemented*
|
||||||
|
3. Default GitHub catalog
|
||||||
|
|
||||||
#### `install_allowed: false` Behavior
|
#### Current Implementation: SPECKIT_CATALOG_URL
|
||||||
|
|
||||||
Extensions from discovery-only catalogs are shown in `specify extension search` results but cannot be installed directly:
|
**The currently available method** for using custom catalogs is the `SPECKIT_CATALOG_URL` environment variable:
|
||||||
|
|
||||||
```
|
|
||||||
⚠ 'linear' is available in the 'community' catalog but installation is not allowed from that catalog.
|
|
||||||
|
|
||||||
To enable installation, add 'linear' to an approved catalog (install_allowed: true) in .specify/extension-catalogs.yml.
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `SPECKIT_CATALOG_URL` (Backward Compatibility)
|
|
||||||
|
|
||||||
The `SPECKIT_CATALOG_URL` environment variable still works — it is treated as a single `install_allowed: true` catalog, **replacing both defaults** for full backward compatibility:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Point to your organization's catalog
|
# Point to your organization's catalog
|
||||||
@@ -1087,15 +1021,11 @@ List installed extensions in current project.
|
|||||||
$ specify extension list
|
$ specify extension list
|
||||||
|
|
||||||
Installed Extensions:
|
Installed Extensions:
|
||||||
✓ Jira Integration (v1.0.0)
|
✓ jira (v1.0.0) - Jira Integration
|
||||||
jira
|
Commands: 3 | Hooks: 2 | Status: Enabled
|
||||||
Create Jira issues from spec-kit artifacts
|
|
||||||
Commands: 3 | Hooks: 2 | Priority: 10 | Status: Enabled
|
|
||||||
|
|
||||||
✓ Linear Integration (v0.9.0)
|
✓ linear (v0.9.0) - Linear Integration
|
||||||
linear
|
Commands: 1 | Hooks: 1 | Status: Enabled
|
||||||
Create Linear issues from spec-kit artifacts
|
|
||||||
Commands: 1 | Hooks: 1 | Priority: 10 | Status: Enabled
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
@@ -1203,9 +1133,10 @@ Next steps:
|
|||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
|
|
||||||
- `--from URL`: Install from a remote URL (archive). Does not accept Git repositories directly.
|
- `--from URL`: Install from custom URL or Git repo
|
||||||
- `--dev`: Install from a local path in development mode (the PATH is the positional `extension` argument).
|
- `--version VERSION`: Install specific version
|
||||||
- `--priority NUMBER`: Set resolution priority (lower = higher precedence, default 10)
|
- `--dev PATH`: Install from local path (development mode)
|
||||||
|
- `--no-register`: Skip command registration (manual setup)
|
||||||
|
|
||||||
#### `specify extension remove NAME`
|
#### `specify extension remove NAME`
|
||||||
|
|
||||||
@@ -1286,29 +1217,6 @@ $ specify extension disable jira
|
|||||||
To re-enable: specify extension enable jira
|
To re-enable: specify extension enable jira
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `specify extension set-priority NAME PRIORITY`
|
|
||||||
|
|
||||||
Change the resolution priority of an installed extension.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ specify extension set-priority jira 5
|
|
||||||
|
|
||||||
✓ Extension 'Jira Integration' priority changed: 10 → 5
|
|
||||||
|
|
||||||
Lower priority = higher precedence in template resolution
|
|
||||||
```
|
|
||||||
|
|
||||||
**Priority Values:**
|
|
||||||
|
|
||||||
- Lower numbers = higher precedence (checked first in resolution)
|
|
||||||
- Default priority is 10
|
|
||||||
- Must be a positive integer (1 or higher)
|
|
||||||
|
|
||||||
**Use Cases:**
|
|
||||||
|
|
||||||
- Ensure a critical extension's templates take precedence
|
|
||||||
- Override default resolution order when multiple extensions provide similar templates
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Compatibility & Versioning
|
## Compatibility & Versioning
|
||||||
@@ -1517,7 +1425,7 @@ specify extension add github-projects
|
|||||||
/speckit.github.taskstoissues
|
/speckit.github.taskstoissues
|
||||||
```
|
```
|
||||||
|
|
||||||
**Migration alias** (if needed):
|
**Compatibility shim** (if needed):
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# extension.yml
|
# extension.yml
|
||||||
@@ -1525,234 +1433,212 @@ provides:
|
|||||||
commands:
|
commands:
|
||||||
- name: "speckit.github.taskstoissues"
|
- name: "speckit.github.taskstoissues"
|
||||||
file: "commands/taskstoissues.md"
|
file: "commands/taskstoissues.md"
|
||||||
aliases: ["speckit.github.sync-taskstoissues"] # Alternate namespaced entry point
|
aliases: ["speckit.taskstoissues"] # Backward compatibility
|
||||||
```
|
```
|
||||||
|
|
||||||
AI agents register both names, so callers can migrate to the alternate alias without relying on deprecated global shortcuts like `/speckit.taskstoissues`.
|
AI agent registers both names, so old scripts work.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Implementation Phases
|
## Implementation Phases
|
||||||
|
|
||||||
### Phase 1: Core Extension System ✅ COMPLETED
|
### Phase 1: Core Extension System (Week 1-2)
|
||||||
|
|
||||||
**Goal**: Basic extension infrastructure
|
**Goal**: Basic extension infrastructure
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Extension manifest schema (`extension.yml`)
|
- [ ] Extension manifest schema (`extension.yml`)
|
||||||
- [x] Extension directory structure
|
- [ ] Extension directory structure
|
||||||
- [x] CLI commands:
|
- [ ] CLI commands:
|
||||||
- [x] `specify extension list`
|
- [ ] `specify extension list`
|
||||||
- [x] `specify extension add` (from URL and local `--dev`)
|
- [ ] `specify extension add` (from URL)
|
||||||
- [x] `specify extension remove`
|
- [ ] `specify extension remove`
|
||||||
- [x] Extension registry (`.specify/extensions/.registry`)
|
- [ ] Extension registry (`.specify/extensions/.registry`)
|
||||||
- [x] Command registration (Claude and 15+ other agents)
|
- [ ] Command registration (Claude only initially)
|
||||||
- [x] Basic validation (manifest schema, compatibility)
|
- [ ] Basic validation (manifest schema, compatibility)
|
||||||
- [x] Documentation (extension development guide)
|
- [ ] Documentation (extension development guide)
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Unit tests for manifest parsing
|
- [ ] Unit tests for manifest parsing
|
||||||
- [x] Integration test: Install dummy extension
|
- [ ] Integration test: Install dummy extension
|
||||||
- [x] Integration test: Register commands with Claude
|
- [ ] Integration test: Register commands with Claude
|
||||||
|
|
||||||
### Phase 2: Jira Extension ✅ COMPLETED
|
### Phase 2: Jira Extension (Week 3)
|
||||||
|
|
||||||
**Goal**: First production extension
|
**Goal**: First production extension
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Create `spec-kit-jira` repository
|
- [ ] Create `spec-kit-jira` repository
|
||||||
- [x] Port Jira functionality to extension
|
- [ ] Port Jira functionality to extension
|
||||||
- [x] Create `jira-config.yml` template
|
- [ ] Create `jira-config.yml` template
|
||||||
- [x] Commands:
|
- [ ] Commands:
|
||||||
- [x] `specstoissues.md`
|
- [ ] `specstoissues.md`
|
||||||
- [x] `discover-fields.md`
|
- [ ] `discover-fields.md`
|
||||||
- [x] `sync-status.md`
|
- [ ] `sync-status.md`
|
||||||
- [x] Helper scripts
|
- [ ] Helper scripts
|
||||||
- [x] Documentation (README, configuration guide, examples)
|
- [ ] Documentation (README, configuration guide, examples)
|
||||||
- [x] Release v3.0.0
|
- [ ] Release v1.0.0
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Test on `eng-msa-ts` project
|
- [ ] Test on `eng-msa-ts` project
|
||||||
- [x] Verify spec→Epic, phase→Story, task→Issue mapping
|
- [ ] Verify spec→Epic, phase→Story, task→Issue mapping
|
||||||
- [x] Test configuration loading and validation
|
- [ ] Test configuration loading and validation
|
||||||
- [x] Test custom field application
|
- [ ] Test custom field application
|
||||||
|
|
||||||
### Phase 3: Extension Catalog ✅ COMPLETED
|
### Phase 3: Extension Catalog (Week 4)
|
||||||
|
|
||||||
**Goal**: Discovery and distribution
|
**Goal**: Discovery and distribution
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
- [ ] Central catalog (`extensions/catalog.json` in spec-kit repo)
|
||||||
- [x] Community catalog (`extensions/catalog.community.json`)
|
- [ ] Catalog fetch and parsing
|
||||||
- [x] Catalog fetch and parsing with multi-catalog support
|
- [ ] CLI commands:
|
||||||
- [x] CLI commands:
|
- [ ] `specify extension search`
|
||||||
- [x] `specify extension search`
|
- [ ] `specify extension info`
|
||||||
- [x] `specify extension info`
|
- [ ] Catalog publishing process (GitHub Action)
|
||||||
- [x] `specify extension catalog list`
|
- [ ] Documentation (how to publish extensions)
|
||||||
- [x] `specify extension catalog add`
|
|
||||||
- [x] `specify extension catalog remove`
|
|
||||||
- [x] Documentation (how to publish extensions)
|
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Test catalog fetch
|
- [ ] Test catalog fetch
|
||||||
- [x] Test extension search/filtering
|
- [ ] Test extension search/filtering
|
||||||
- [x] Test catalog caching
|
- [ ] Test catalog caching
|
||||||
- [x] Test multi-catalog merge with priority
|
|
||||||
|
|
||||||
### Phase 4: Advanced Features ✅ COMPLETED
|
### Phase 4: Advanced Features (Week 5-6)
|
||||||
|
|
||||||
**Goal**: Hooks, updates, multi-agent support
|
**Goal**: Hooks, updates, multi-agent support
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Hook system (`hooks` in extension.yml)
|
- [ ] Hook system (`hooks` in extension.yml)
|
||||||
- [x] Hook registration and execution
|
- [ ] Hook registration and execution
|
||||||
- [x] Project extensions config (`.specify/extensions.yml`)
|
- [ ] Project extensions config (`.specify/extensions.yml`)
|
||||||
- [x] CLI commands:
|
- [ ] CLI commands:
|
||||||
- [x] `specify extension update` (with atomic backup/restore)
|
- [ ] `specify extension update`
|
||||||
- [x] `specify extension enable/disable`
|
- [ ] `specify extension enable/disable`
|
||||||
- [x] Command registration for multiple agents (15+ agents including Claude, Copilot, Gemini, Cursor, etc.)
|
- [ ] Command registration for multiple agents (Gemini, Copilot)
|
||||||
- [x] Extension update notifications (version comparison)
|
- [ ] Extension update notifications
|
||||||
- [x] Configuration layer resolution (project, local, env)
|
- [ ] Configuration layer resolution (project, local, env)
|
||||||
|
|
||||||
**Additional features implemented beyond original RFC**:
|
|
||||||
|
|
||||||
- [x] **Display name resolution**: All commands accept extension display names in addition to IDs
|
|
||||||
- [x] **Ambiguous name handling**: User-friendly tables when multiple extensions match a name
|
|
||||||
- [x] **Atomic update with rollback**: Full backup of extension dir, commands, hooks, and registry with automatic rollback on failure
|
|
||||||
- [x] **Pre-install ID validation**: Validates extension ID from ZIP before installing (security)
|
|
||||||
- [x] **Enabled state preservation**: Disabled extensions stay disabled after update
|
|
||||||
- [x] **Registry update/restore methods**: Clean API for enable/disable and rollback operations
|
|
||||||
- [x] **Catalog error fallback**: `extension info` falls back to local info when catalog unavailable
|
|
||||||
- [x] **`_install_allowed` flag**: Discovery-only catalogs can't be used for installation
|
|
||||||
- [x] **Cache invalidation**: Cache invalidated when `SPECKIT_CATALOG_URL` changes
|
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] Test hooks in core commands
|
- [ ] Test hooks in core commands
|
||||||
- [x] Test extension updates (preserve config)
|
- [ ] Test extension updates (preserve config)
|
||||||
- [x] Test multi-agent registration
|
- [ ] Test multi-agent registration
|
||||||
- [x] Test atomic rollback on update failure
|
|
||||||
- [x] Test enabled state preservation
|
|
||||||
- [x] Test display name resolution
|
|
||||||
|
|
||||||
### Phase 5: Polish & Documentation ✅ COMPLETED
|
### Phase 5: Polish & Documentation (Week 7)
|
||||||
|
|
||||||
**Goal**: Production ready
|
**Goal**: Production ready
|
||||||
|
|
||||||
**Deliverables**:
|
**Deliverables**:
|
||||||
|
|
||||||
- [x] Comprehensive documentation:
|
- [ ] Comprehensive documentation:
|
||||||
- [x] User guide (EXTENSION-USER-GUIDE.md)
|
- [ ] User guide (installing/using extensions)
|
||||||
- [x] Extension development guide (EXTENSION-DEV-GUIDE.md)
|
- [ ] Extension development guide
|
||||||
- [x] Extension API reference (EXTENSION-API-REFERENCE.md)
|
- [ ] Extension API reference
|
||||||
- [x] Error messages and validation improvements
|
- [ ] Migration guide (core → extension)
|
||||||
- [x] CLI help text updates
|
- [ ] Error messages and validation improvements
|
||||||
|
- [ ] CLI help text updates
|
||||||
|
- [ ] Example extension template (cookiecutter)
|
||||||
|
- [ ] Blog post / announcement
|
||||||
|
- [ ] Video tutorial
|
||||||
|
|
||||||
**Testing**:
|
**Testing**:
|
||||||
|
|
||||||
- [x] End-to-end testing on multiple projects
|
- [ ] End-to-end testing on multiple projects
|
||||||
- [x] 163 unit tests passing
|
- [ ] Community beta testing
|
||||||
|
- [ ] Performance testing (large projects)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Resolved Questions
|
## Open Questions
|
||||||
|
|
||||||
The following questions from the original RFC have been resolved during implementation:
|
### 1. Extension Namespace
|
||||||
|
|
||||||
### 1. Extension Namespace ✅ RESOLVED
|
|
||||||
|
|
||||||
**Question**: Should extension commands use namespace prefix?
|
**Question**: Should extension commands use namespace prefix?
|
||||||
|
|
||||||
**Decision**: **Option C** - Both prefixed and aliases are supported. Commands use `speckit.{extension}.{command}` as canonical name, with optional aliases defined in manifest.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: The `aliases` field in `extension.yml` allows extensions to register additional command names.
|
- A) Prefixed: `/speckit.jira.specstoissues` (explicit, avoids conflicts)
|
||||||
|
- B) Short alias: `/jira.specstoissues` (shorter, less verbose)
|
||||||
|
- C) Both: Register both names, prefer prefixed in docs
|
||||||
|
|
||||||
|
**Recommendation**: C (both), prefixed is canonical
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. Config File Location ✅ RESOLVED
|
### 2. Config File Location
|
||||||
|
|
||||||
**Question**: Where should extension configs live?
|
**Question**: Where should extension configs live?
|
||||||
|
|
||||||
**Decision**: **Option A** - Extension directory (`.specify/extensions/{ext-id}/{ext-id}-config.yml`). This keeps extensions self-contained and easier to manage.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: Each extension has its own config file within its directory, with layered resolution (defaults → project → local → env vars).
|
- A) Extension directory: `.specify/extensions/jira/jira-config.yml` (encapsulated)
|
||||||
|
- B) Root level: `.specify/jira-config.yml` (more visible)
|
||||||
|
- C) Unified: `.specify/extensions.yml` (all extension configs in one file)
|
||||||
|
|
||||||
|
**Recommendation**: A (extension directory), cleaner separation
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3. Command File Format ✅ RESOLVED
|
### 3. Command File Format
|
||||||
|
|
||||||
**Question**: Should extensions use universal format or agent-specific?
|
**Question**: Should extensions use universal format or agent-specific?
|
||||||
|
|
||||||
**Decision**: **Option A** - Universal Markdown format. Extensions write commands once, CLI converts to agent-specific format during registration.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: `CommandRegistrar` class handles conversion to 15+ agent formats (Claude, Copilot, Gemini, Cursor, etc.).
|
- A) Universal Markdown: Extensions write once, CLI converts per-agent
|
||||||
|
- B) Agent-specific: Extensions provide separate files for each agent
|
||||||
|
- C) Hybrid: Universal default, agent-specific overrides
|
||||||
|
|
||||||
|
**Recommendation**: A (universal), reduces duplication
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 4. Hook Execution Model ✅ RESOLVED
|
### 4. Hook Execution Model
|
||||||
|
|
||||||
**Question**: How should hooks execute?
|
**Question**: How should hooks execute?
|
||||||
|
|
||||||
**Decision**: **Option A** - Hooks are registered in `.specify/extensions.yml` and executed by the AI agent when it sees the hook trigger. Hook state (enabled/disabled) is managed per-extension.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: `HookExecutor` class manages hook registration and state in `extensions.yml`.
|
- A) AI agent interprets: Core commands output `EXECUTE_COMMAND: name`
|
||||||
|
- B) CLI executes: Core commands call `specify extension hook after_tasks`
|
||||||
|
- C) Agent built-in: Extension system built into AI agent (Claude SDK)
|
||||||
|
|
||||||
|
**Recommendation**: A initially (simpler), move to C long-term
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. Extension Distribution ✅ RESOLVED
|
### 5. Extension Distribution
|
||||||
|
|
||||||
**Question**: How should extensions be packaged?
|
**Question**: How should extensions be packaged?
|
||||||
|
|
||||||
**Decision**: **Option A** - ZIP archives downloaded from GitHub releases (via catalog `download_url`). Local development uses `--dev` flag with directory path.
|
**Options**:
|
||||||
|
|
||||||
**Implementation**: `ExtensionManager.install_from_zip()` handles ZIP extraction and validation.
|
- A) ZIP archives: Downloaded from GitHub releases
|
||||||
|
- B) Git repos: Cloned directly (`git clone`)
|
||||||
|
- C) Python packages: Installable via `uv tool install`
|
||||||
|
|
||||||
|
**Recommendation**: A (ZIP), simpler for non-Python extensions in future
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. Multi-Version Support ✅ RESOLVED
|
### 6. Multi-Version Support
|
||||||
|
|
||||||
**Question**: Can multiple versions of same extension coexist?
|
**Question**: Can multiple versions of same extension coexist?
|
||||||
|
|
||||||
**Decision**: **Option A** - Single version only. Updates replace the existing version with atomic rollback on failure.
|
|
||||||
|
|
||||||
**Implementation**: `extension update` performs atomic backup/restore to ensure safe updates.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Open Questions (Remaining)
|
|
||||||
|
|
||||||
### 1. Sandboxing / Permissions (Future)
|
|
||||||
|
|
||||||
**Question**: Should extensions declare required permissions?
|
|
||||||
|
|
||||||
**Options**:
|
**Options**:
|
||||||
|
|
||||||
- A) No sandboxing (current): Extensions run with same privileges as AI agent
|
- A) Single version: Only one version installed at a time
|
||||||
- B) Permission declarations: Extensions declare `filesystem:read`, `network:external`, etc.
|
- B) Multi-version: Side-by-side versions (`.specify/extensions/jira@1.0/`, `.specify/extensions/jira@2.0/`)
|
||||||
- C) Opt-in sandboxing: Organizations can enable permission enforcement
|
- C) Per-branch: Different branches use different versions
|
||||||
|
|
||||||
**Status**: Deferred to future version. Currently using trust-based model where users trust extension authors.
|
**Recommendation**: A initially (simpler), consider B in future if needed
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. Package Signatures (Future)
|
|
||||||
|
|
||||||
**Question**: Should extensions be cryptographically signed?
|
|
||||||
|
|
||||||
**Options**:
|
|
||||||
|
|
||||||
- A) No signatures (current): Trust based on catalog source
|
|
||||||
- B) GPG/Sigstore signatures: Verify package integrity
|
|
||||||
- C) Catalog-level verification: Catalog maintainers verify packages
|
|
||||||
|
|
||||||
**Status**: Deferred to future version. `checksum` field is available in catalog schema but not enforced.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +1,6 @@
|
|||||||
{
|
{
|
||||||
"schema_version": "1.0",
|
"schema_version": "1.0",
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
"updated_at": "2026-02-03T00:00:00Z",
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
||||||
"extensions": {
|
"extensions": {}
|
||||||
"selftest": {
|
}
|
||||||
"name": "Spec Kit Self-Test Utility",
|
|
||||||
"id": "selftest",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.",
|
|
||||||
"author": "spec-kit-core",
|
|
||||||
"repository": "https://github.com/github/spec-kit",
|
|
||||||
"download_url": "https://github.com/github/spec-kit/releases/download/selftest-v1.0.0/selftest.zip",
|
|
||||||
"tags": [
|
|
||||||
"testing",
|
|
||||||
"core",
|
|
||||||
"utility"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Validate the lifecycle of an extension from the catalog."
|
|
||||||
---
|
|
||||||
|
|
||||||
# Extension Self-Test: `$ARGUMENTS`
|
|
||||||
|
|
||||||
This command drives a self-test simulating the developer experience with the `$ARGUMENTS` extension.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Validate the end-to-end lifecycle (discovery, installation, registration) for the extension: `$ARGUMENTS`.
|
|
||||||
If `$ARGUMENTS` is empty, you must tell the user to provide an extension name, for example: `/speckit.selftest.extension linear`.
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
### Step 1: Catalog Discovery Validation
|
|
||||||
|
|
||||||
Check if the extension exists in the Spec Kit catalog.
|
|
||||||
Execute this command and verify that it completes successfully and that the returned extension ID exactly matches `$ARGUMENTS`. If the command fails or the ID does not match `$ARGUMENTS`, fail the test.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension info "$ARGUMENTS"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Simulate Installation
|
|
||||||
|
|
||||||
First, try to add the extension to the current workspace configuration directly. If the catalog provides the extension as `install_allowed: false` (discovery-only), this step is *expected* to fail.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension add "$ARGUMENTS"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, simulate adding the extension by installing it from its catalog download URL, which should bypass the restriction.
|
|
||||||
Obtain the extension's `download_url` from the catalog metadata (for example, via a catalog info command or UI), then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify extension add "$ARGUMENTS" --from "<download_url>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Registration Verification
|
|
||||||
|
|
||||||
Once the `add` command completes, verify the installation by checking the project configuration.
|
|
||||||
Use terminal tools (like `cat`) to verify that the following file contains a record for `$ARGUMENTS`.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat .specify/extensions/.registry/$ARGUMENTS.json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Verification Report
|
|
||||||
|
|
||||||
Analyze the standard output of the three steps.
|
|
||||||
Generate a terminal-style test output format detailing the results of discovery, installation, and registration. Return this directly to the user.
|
|
||||||
|
|
||||||
Example output format:
|
|
||||||
```text
|
|
||||||
============================= test session starts ==============================
|
|
||||||
collected 3 items
|
|
||||||
|
|
||||||
test_selftest_discovery.py::test_catalog_search [PASS/FAIL]
|
|
||||||
Details: [Provide execution result of specify extension search]
|
|
||||||
|
|
||||||
test_selftest_installation.py::test_extension_add [PASS/FAIL]
|
|
||||||
Details: [Provide execution result of specify extension add]
|
|
||||||
|
|
||||||
test_selftest_registration.py::test_config_verification [PASS/FAIL]
|
|
||||||
Details: [Provide execution result of registry record verification]
|
|
||||||
|
|
||||||
============================== [X] passed in ... ==============================
|
|
||||||
```
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
schema_version: "1.0"
|
|
||||||
extension:
|
|
||||||
id: selftest
|
|
||||||
name: Spec Kit Self-Test Utility
|
|
||||||
version: 1.0.0
|
|
||||||
description: Verifies catalog extensions by programmatically walking through the discovery, installation, and registration lifecycle.
|
|
||||||
author: spec-kit-core
|
|
||||||
repository: https://github.com/github/spec-kit
|
|
||||||
license: MIT
|
|
||||||
requires:
|
|
||||||
speckit_version: ">=0.2.0"
|
|
||||||
provides:
|
|
||||||
commands:
|
|
||||||
- name: speckit.selftest.extension
|
|
||||||
file: commands/selftest.md
|
|
||||||
description: Validate the lifecycle of an extension from the catalog.
|
|
||||||
@@ -47,8 +47,8 @@ provides:
|
|||||||
- name: "speckit.my-extension.example"
|
- name: "speckit.my-extension.example"
|
||||||
file: "commands/example.md"
|
file: "commands/example.md"
|
||||||
description: "Example command that demonstrates functionality"
|
description: "Example command that demonstrates functionality"
|
||||||
# Optional: Add aliases in the same namespaced format
|
# Optional: Add aliases for shorter command names
|
||||||
aliases: ["speckit.my-extension.example-short"]
|
aliases: ["speckit.example"]
|
||||||
|
|
||||||
# ADD MORE COMMANDS: Copy this block for each command
|
# ADD MORE COMMANDS: Copy this block for each command
|
||||||
# - name: "speckit.my-extension.another-command"
|
# - name: "speckit.my-extension.another-command"
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
# Spec Kit - February 2026 Newsletter
|
|
||||||
|
|
||||||
This edition covers Spec Kit activity in February 2026. Versions v0.1.7 through v0.1.13 shipped during the month, addressing bugs and adding features including a dual-catalog extension system and additional agent integrations. Community activity included blog posts, tutorials, and meetup sessions. A category summary is in the table below, followed by details.
|
|
||||||
|
|
||||||
| **Spec Kit Core (Feb 2026)** | **Community & Content** | **Roadmap & Next** |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Versions **v0.1.7** through **v0.1.13** shipped with bug fixes and features, including a **dual-catalog extension system** and new agent integrations. Over 300 issues were closed (of ~800 filed). The repo reached 71k stars and 6.4k forks. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit/issues) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit) | Eduardo Luz published a LinkedIn article on SDD and Spec Kit [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en). Erick Matsen blogged a walkthrough of building a bioinformatics pipeline with Spec Kit [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html). Microsoft MVP [Eric Boyd](https://ericboyd.com/) (not the Microsoft AI Platform VP of the same name) presented at the Cleveland .NET User Group [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit). | **v0.2.0** was released in early March, consolidating February's work. It added extensions for Jira and Azure DevOps, community plugin support, and agents for Tabnine CLI and Kiro CLI [\[github.com\]](https://github.com/github/spec-kit/releases). Future work includes spec lifecycle management and progress toward a stable 1.0 release [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html). |
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
## Spec Kit Project Updates
|
|
||||||
|
|
||||||
Spec Kit released versions **v0.1.7** through **v0.1.13** during February. Version 0.1.7 (early February) updated documentation for the newly introduced **dual-catalog extension system**, which allows both core and community extension catalogs to coexist. Subsequent patches (0.1.8, 0.1.9, etc.) bumped dependencies such as GitHub Actions versions and resolved minor issues. **v0.1.10** fixed YAML front-matter handling in generated files. By late February, **v0.1.12** and **v0.1.13** shipped with additional fixes in preparation for the next version bump. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
|
||||||
|
|
||||||
The main architectural addition was the **modular extension system** with separate "core" and "community" extension catalogs for third-party add-ons. Multiple community-contributed extensions were merged during the month, including a **Jira extension** for issue tracker integration, an **Azure DevOps extension**, and utility extensions for code review, retrospective documentation, and CI/CD sync. The pending 0.2.0 release changelog lists over a dozen changes from February, including the extension additions and support for **multiple agent catalogs concurrently**. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
|
||||||
|
|
||||||
By end of February, **over 330 issues/feature requests had been closed on GitHub** (out of ~870 filed to date). External contributors submitted pull requests including the **Tabnine CLI support**, which was merged in late February. The repository reached ~71k stars and crossed 6,000 forks. [\[github.com\]](https://github.com/github/spec-kit/issues) [\[github.com\]](https://github.com/github/spec-kit/releases) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
|
|
||||||
|
|
||||||
On the stability side, February's work focused on tightening core workflows and fixing edge-case bugs in the specification, planning, and task-generation commands. The team addressed file-handling issues (e.g., clarifying how output files are created/appended) and improved the reliability of the automated release pipeline. The project also added **Kiro CLI** to the supported agent list and updated integration scripts for Cursor and Code Interpreter, bringing the total number of supported AI coding assistants to over 20. [\[github.com\]](https://github.com/github/spec-kit/releases) [\[github.com\]](https://github.com/github/spec-kit)
|
|
||||||
|
|
||||||
## Community & Content
|
|
||||||
|
|
||||||
**Eduardo Luz** published a LinkedIn article on Feb 15 titled *"Specification Driven Development (SDD) and the GitHub Spec Kit: Elevating Software Engineering."* The article draws on his experience as a senior engineer to describe common causes of technical debt and inconsistent designs, and how SDD addresses them. It walks through Spec Kit's **four-layer approach** (Constitution, Design, Tasks, Implementation) and discusses treating specifications as a source of truth. The post generated discussion among software architects on LinkedIn about reducing misunderstandings and rework through spec-driven workflows. [\[linkedin.com\]](https://www.linkedin.com/pulse/specification-driven-development-sdd-github-spec-kit-elevating-luz-tojmc?tl=en)
|
|
||||||
|
|
||||||
**Erick Matsen** (Fred Hutchinson Cancer Center) posted a detailed walkthrough on Feb 10 titled *"Spec-Driven Development with spec-kit."* He describes building a **bioinformatics pipeline** in a single day using Spec Kit's workflow (from `speckit.constitution` to `speckit.implement`). The post includes command outputs and notes on decisions made along the way, such as refining the spec to add domain-specific requirements. He writes: "I really recommend this approach. This feels like the way software development should be." [\[matsen.fredhutch.org\]](https://matsen.fredhutch.org/general/2026/02/10/spec-kit-walkthrough.html) [\[github.com\]](https://github.com/mnriem/spec-kit-dotnet-cli-demo)
|
|
||||||
|
|
||||||
Several other tutorials and guides appeared during the month. An article on *IntuitionLabs* (updated Feb 21) provided a guide to Spec Kit covering the philosophy behind SDD and a walkthrough of the four-phase workflow with examples. A piece by Ry Walker (Feb 22) summarized key aspects of Spec Kit, noting its agent-agnostic design and 71k-star count. Microsoft's Developer Blog post from late 2025 (*"Diving Into Spec-Driven Development with GitHub Spec Kit"* by Den Delimarsky) continued to circulate among new users. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[rywalker.com\]](https://rywalker.com/research/github-spec-kit)
|
|
||||||
|
|
||||||
On **Feb 25**, the Cleveland C# .NET User Group hosted a session titled *"Spec Driven Development with GitHub Spec Kit."* The talk was delivered by Microsoft MVP **[Eric Boyd](https://ericboyd.com/)** (Cleveland-based .NET developer; not to be confused with the Microsoft AI Platform VP of the same name). Boyd covered how specs change an AI coding assistant's output, patterns for iterating and refining specs over multiple cycles, and moving from ad-hoc prompting to a repeatable spec-driven workflow. Other groups, including GDG Madison, also listed sessions on spec-driven development in late February and early March. [\[ericboyd.com\]](https://ericboyd.com/events/cleveland-csharp-user-group-february-25-2026-spec-driven-development-sdd-github-spec-kit)
|
|
||||||
|
|
||||||
On GitHub, the **Spec Kit Discussions forum** saw activity around installation troubleshooting, handling multi-feature projects with Spec Kit's branching model, and feature suggestions. One thread discussed how Spec Kit treats each spec as a short-lived artifact tied to a feature branch, which led to discussion about future support for long-running "spec of record" use cases. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
|
||||||
|
|
||||||
## SDD Ecosystem
|
|
||||||
|
|
||||||
Other spec-driven development tools also saw activity in February.
|
|
||||||
|
|
||||||
AWS **Kiro** released version 0.10 on Feb 18 with two new spec workflows: a **Design-First** mode (starting from architecture/pseudocode to derive requirements) and a **Bugfix** mode (structured root-cause analysis producing a `bugfix.md` spec file). Kiro also added hunk-level code review for AI-generated changes and pre/post task hooks for custom automation. AWS expanded Kiro to GovCloud regions on Feb 17 for government compliance use cases. [\[kiro.dev\]](https://kiro.dev/changelog/)
|
|
||||||
|
|
||||||
**OpenSpec** (by Fission AI), a lightweight SDD framework, reached ~29.3k stars and nearly 2k forks. Its community published guides and comparisons during the month, including *"Spec-Driven Development Made Easy: A Practical Guide with OpenSpec."* OpenSpec emphasizes simplicity and flexibility, integrating with multiple AI coding assistants via YAML configs.
|
|
||||||
|
|
||||||
**Tessl** remained in private beta. As described by Thoughtworks writer Birgitta Boeckeler, Tessl pursues a **spec-as-source** model where specifications are maintained long-term and directly generate code files one-to-one, with generated code labeled as "do not edit." This contrasts with Spec Kit's current approach of creating specs per feature/branch. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
|
||||||
|
|
||||||
An **arXiv preprint** (January 2026) categorized SDD implementations into three levels: *spec-first*, *spec-anchored*, and *spec-as-source*. Spec Kit was identified as primarily spec-first with elements of spec-anchored. Tech media published reviews including a *Vibe Coding* "GitHub Spec Kit Review (2026)" and a blog post titled *"Putting Spec Kit Through Its Paces: Radical Idea or Reinvented Waterfall?"* which concluded that SDD with AI assistance is more iterative than traditional Waterfall. [\[intuitionlabs.ai\]](https://intuitionlabs.ai/articles/spec-driven-development-spec-kit) [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
**v0.2.0** was released on March 10, 2026, consolidating the month's work. It includes new extensions (Jira, Azure DevOps, review, sync), support for multiple extension catalogs and community plugins, and additional agent integrations (Tabnine CLI, Kiro CLI). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
|
||||||
|
|
||||||
Areas under discussion or in progress for future development:
|
|
||||||
|
|
||||||
- **Spec lifecycle management** -- supporting longer-lived specifications that can evolve across multiple iterations, rather than being tied to a single feature branch. Users have raised this in GitHub Discussions, and the concept of "spec-anchored" development is under consideration. [\[martinfowler.com\]](https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html)
|
|
||||||
- **CI/CD integration** -- incorporating Spec Kit verification (e.g., `speckit.checklist` or `speckit.verify`) into pull request workflows and project management tools. February's Jira and Azure DevOps extensions are a step in this direction. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
|
||||||
- **Continued agent support** -- adding integrations as new AI coding assistants emerge. The project currently supports over 20 agents and has been adding new ones (Kiro CLI, Tabnine CLI) as they become available. [\[github.com\]](https://github.com/github/spec-kit)
|
|
||||||
- **Community ecosystem** -- the open extension model allows external contributors to add functionality directly. February's Jira and Azure DevOps plugins were community-contributed. The Spec Kit README now links to community walkthrough demos for .NET, Spring Boot, and other stacks. [\[github.com\]](https://github.com/github/spec-kit)
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# Preset System Architecture
|
|
||||||
|
|
||||||
This document describes the internal architecture of the preset system — how template resolution, command registration, and catalog management work under the hood.
|
|
||||||
|
|
||||||
For usage instructions, see [README.md](README.md).
|
|
||||||
|
|
||||||
## Template Resolution
|
|
||||||
|
|
||||||
When Spec Kit needs a template (e.g. `spec-template`), the `PresetResolver` walks a priority stack and returns the first match:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["resolve_template('spec-template')"] --> B{Override exists?}
|
|
||||||
B -- Yes --> C[".specify/templates/overrides/spec-template.md"]
|
|
||||||
B -- No --> D{Preset provides it?}
|
|
||||||
D -- Yes --> E[".specify/presets/‹preset-id›/templates/spec-template.md"]
|
|
||||||
D -- No --> F{Extension provides it?}
|
|
||||||
F -- Yes --> G[".specify/extensions/‹ext-id›/templates/spec-template.md"]
|
|
||||||
F -- No --> H[".specify/templates/spec-template.md"]
|
|
||||||
|
|
||||||
E -- "multiple presets?" --> I["lowest priority number wins"]
|
|
||||||
I --> E
|
|
||||||
|
|
||||||
style C fill:#4caf50,color:#fff
|
|
||||||
style E fill:#2196f3,color:#fff
|
|
||||||
style G fill:#ff9800,color:#fff
|
|
||||||
style H fill:#9e9e9e,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
| Priority | Source | Path | Use case |
|
|
||||||
|----------|--------|------|----------|
|
|
||||||
| 1 (highest) | Override | `.specify/templates/overrides/` | One-off project-local tweaks |
|
|
||||||
| 2 | Preset | `.specify/presets/<id>/templates/` | Shareable, stackable customizations |
|
|
||||||
| 3 | Extension | `.specify/extensions/<id>/templates/` | Extension-provided templates |
|
|
||||||
| 4 (lowest) | Core | `.specify/templates/` | Shipped defaults |
|
|
||||||
|
|
||||||
When multiple presets are installed, they're sorted by their `priority` field (lower number = higher precedence). This is set via `--priority` on `specify preset add`.
|
|
||||||
|
|
||||||
The resolution is implemented three times to ensure consistency:
|
|
||||||
- **Python**: `PresetResolver` in `src/specify_cli/presets.py`
|
|
||||||
- **Bash**: `resolve_template()` in `scripts/bash/common.sh`
|
|
||||||
- **PowerShell**: `Resolve-Template` in `scripts/powershell/common.ps1`
|
|
||||||
|
|
||||||
## Command Registration
|
|
||||||
|
|
||||||
When a preset is installed with `type: "command"` entries, the `PresetManager` registers them into all detected agent directories using the shared `CommandRegistrar` from `src/specify_cli/agents.py`.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["specify preset add my-preset"] --> B{Preset has type: command?}
|
|
||||||
B -- No --> Z["done (templates only)"]
|
|
||||||
B -- Yes --> C{Extension command?}
|
|
||||||
C -- "speckit.myext.cmd\n(3+ dot segments)" --> D{Extension installed?}
|
|
||||||
D -- No --> E["skip (extension not active)"]
|
|
||||||
D -- Yes --> F["register command"]
|
|
||||||
C -- "speckit.specify\n(core command)" --> F
|
|
||||||
F --> G["detect agent directories"]
|
|
||||||
G --> H[".claude/commands/"]
|
|
||||||
G --> I[".gemini/commands/"]
|
|
||||||
G --> J[".github/agents/"]
|
|
||||||
G --> K["... (17+ agents)"]
|
|
||||||
H --> L["write .md (Markdown format)"]
|
|
||||||
I --> M["write .toml (TOML format)"]
|
|
||||||
J --> N["write .agent.md + .prompt.md"]
|
|
||||||
|
|
||||||
style E fill:#ff5722,color:#fff
|
|
||||||
style L fill:#4caf50,color:#fff
|
|
||||||
style M fill:#4caf50,color:#fff
|
|
||||||
style N fill:#4caf50,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extension safety check
|
|
||||||
|
|
||||||
Command names follow the pattern `speckit.<ext-id>.<cmd-name>`. When a command has 3+ dot segments, the system extracts the extension ID and checks if `.specify/extensions/<ext-id>/` exists. If the extension isn't installed, the command is skipped — preventing orphan files referencing non-existent extensions.
|
|
||||||
|
|
||||||
Core commands (e.g. `speckit.specify`, with only 2 segments) are always registered.
|
|
||||||
|
|
||||||
### Agent format rendering
|
|
||||||
|
|
||||||
The `CommandRegistrar` renders commands differently per agent:
|
|
||||||
|
|
||||||
| Agent | Format | Extension | Arg placeholder |
|
|
||||||
|-------|--------|-----------|-----------------|
|
|
||||||
| Claude, Cursor, opencode, Windsurf, etc. | Markdown | `.md` | `$ARGUMENTS` |
|
|
||||||
| Copilot | Markdown | `.agent.md` + `.prompt.md` | `$ARGUMENTS` |
|
|
||||||
| Gemini, Qwen, Tabnine | TOML | `.toml` | `{{args}}` |
|
|
||||||
|
|
||||||
### Cleanup on removal
|
|
||||||
|
|
||||||
When `specify preset remove` is called, the registered commands are read from the registry metadata and the corresponding files are deleted from each agent directory, including Copilot companion `.prompt.md` files.
|
|
||||||
|
|
||||||
## Catalog System
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart TD
|
|
||||||
A["specify preset search"] --> B["PresetCatalog.get_active_catalogs()"]
|
|
||||||
B --> C{SPECKIT_PRESET_CATALOG_URL set?}
|
|
||||||
C -- Yes --> D["single custom catalog"]
|
|
||||||
C -- No --> E{.specify/preset-catalogs.yml exists?}
|
|
||||||
E -- Yes --> F["project-level catalog stack"]
|
|
||||||
E -- No --> G{"~/.specify/preset-catalogs.yml exists?"}
|
|
||||||
G -- Yes --> H["user-level catalog stack"]
|
|
||||||
G -- No --> I["built-in defaults"]
|
|
||||||
I --> J["default (install allowed)"]
|
|
||||||
I --> K["community (discovery only)"]
|
|
||||||
|
|
||||||
style D fill:#ff9800,color:#fff
|
|
||||||
style F fill:#2196f3,color:#fff
|
|
||||||
style H fill:#2196f3,color:#fff
|
|
||||||
style J fill:#4caf50,color:#fff
|
|
||||||
style K fill:#9e9e9e,color:#fff
|
|
||||||
```
|
|
||||||
|
|
||||||
Catalogs are fetched with a 1-hour cache (per-URL, SHA256-hashed cache files). Each catalog entry has a `priority` (for merge ordering) and `install_allowed` flag.
|
|
||||||
|
|
||||||
## Repository Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
presets/
|
|
||||||
├── ARCHITECTURE.md # This file
|
|
||||||
├── PUBLISHING.md # Guide for submitting presets to the catalog
|
|
||||||
├── README.md # User guide
|
|
||||||
├── catalog.json # Official preset catalog
|
|
||||||
├── catalog.community.json # Community preset catalog
|
|
||||||
├── scaffold/ # Scaffold for creating new presets
|
|
||||||
│ ├── preset.yml # Example manifest
|
|
||||||
│ ├── README.md # Guide for customizing the scaffold
|
|
||||||
│ ├── commands/
|
|
||||||
│ │ ├── speckit.specify.md # Core command override example
|
|
||||||
│ │ └── speckit.myext.myextcmd.md # Extension command override example
|
|
||||||
│ └── templates/
|
|
||||||
│ ├── spec-template.md # Core template override example
|
|
||||||
│ └── myext-template.md # Extension template override example
|
|
||||||
└── self-test/ # Self-test preset (overrides all core templates)
|
|
||||||
├── preset.yml
|
|
||||||
├── commands/
|
|
||||||
│ └── speckit.specify.md
|
|
||||||
└── templates/
|
|
||||||
├── spec-template.md
|
|
||||||
├── plan-template.md
|
|
||||||
├── tasks-template.md
|
|
||||||
├── checklist-template.md
|
|
||||||
├── constitution-template.md
|
|
||||||
└── agent-file-template.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/specify_cli/
|
|
||||||
├── agents.py # CommandRegistrar — shared infrastructure for writing
|
|
||||||
│ # command files to agent directories
|
|
||||||
├── presets.py # PresetManifest, PresetRegistry, PresetManager,
|
|
||||||
│ # PresetCatalog, PresetCatalogEntry, PresetResolver
|
|
||||||
└── __init__.py # CLI commands: specify preset list/add/remove/search/
|
|
||||||
# resolve/info, specify preset catalog list/add/remove
|
|
||||||
```
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
# Preset Publishing Guide
|
|
||||||
|
|
||||||
This guide explains how to publish your preset to the Spec Kit preset catalog, making it discoverable by `specify preset search`.
|
|
||||||
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
1. [Prerequisites](#prerequisites)
|
|
||||||
2. [Prepare Your Preset](#prepare-your-preset)
|
|
||||||
3. [Submit to Catalog](#submit-to-catalog)
|
|
||||||
4. [Verification Process](#verification-process)
|
|
||||||
5. [Release Workflow](#release-workflow)
|
|
||||||
6. [Best Practices](#best-practices)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
Before publishing a preset, ensure you have:
|
|
||||||
|
|
||||||
1. **Valid Preset**: A working preset with a valid `preset.yml` manifest
|
|
||||||
2. **Git Repository**: Preset hosted on GitHub (or other public git hosting)
|
|
||||||
3. **Documentation**: README.md with description and usage instructions
|
|
||||||
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
|
|
||||||
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
|
|
||||||
6. **Testing**: Preset tested on real projects with `specify preset add --dev`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prepare Your Preset
|
|
||||||
|
|
||||||
### 1. Preset Structure
|
|
||||||
|
|
||||||
Ensure your preset follows the standard structure:
|
|
||||||
|
|
||||||
```text
|
|
||||||
your-preset/
|
|
||||||
├── preset.yml # Required: Preset manifest
|
|
||||||
├── README.md # Required: Documentation
|
|
||||||
├── LICENSE # Required: License file
|
|
||||||
├── CHANGELOG.md # Recommended: Version history
|
|
||||||
│
|
|
||||||
├── templates/ # Template overrides
|
|
||||||
│ ├── spec-template.md
|
|
||||||
│ ├── plan-template.md
|
|
||||||
│ └── ...
|
|
||||||
│
|
|
||||||
└── commands/ # Command overrides (optional)
|
|
||||||
└── speckit.specify.md
|
|
||||||
```
|
|
||||||
|
|
||||||
Start from the [scaffold](scaffold/) if you're creating a new preset.
|
|
||||||
|
|
||||||
### 2. preset.yml Validation
|
|
||||||
|
|
||||||
Verify your manifest is valid:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
schema_version: "1.0"
|
|
||||||
|
|
||||||
preset:
|
|
||||||
id: "your-preset" # Unique lowercase-hyphenated ID
|
|
||||||
name: "Your Preset Name" # Human-readable name
|
|
||||||
version: "1.0.0" # Semantic version
|
|
||||||
description: "Brief description (one sentence)"
|
|
||||||
author: "Your Name or Organization"
|
|
||||||
repository: "https://github.com/your-org/spec-kit-preset-your-preset"
|
|
||||||
license: "MIT"
|
|
||||||
|
|
||||||
requires:
|
|
||||||
speckit_version: ">=0.1.0" # Required spec-kit version
|
|
||||||
|
|
||||||
provides:
|
|
||||||
templates:
|
|
||||||
- type: "template"
|
|
||||||
name: "spec-template"
|
|
||||||
file: "templates/spec-template.md"
|
|
||||||
description: "Custom spec template"
|
|
||||||
replaces: "spec-template"
|
|
||||||
|
|
||||||
tags: # 2-5 relevant tags
|
|
||||||
- "category"
|
|
||||||
- "workflow"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Validation Checklist**:
|
|
||||||
|
|
||||||
- ✅ `id` is lowercase with hyphens only (no underscores, spaces, or special characters)
|
|
||||||
- ✅ `version` follows semantic versioning (X.Y.Z)
|
|
||||||
- ✅ `description` is concise (under 200 characters)
|
|
||||||
- ✅ `repository` URL is valid and public
|
|
||||||
- ✅ All template and command files exist in the preset directory
|
|
||||||
- ✅ Template names are lowercase with hyphens only
|
|
||||||
- ✅ Command names use dot notation (e.g. `speckit.specify`)
|
|
||||||
- ✅ Tags are lowercase and descriptive
|
|
||||||
|
|
||||||
### 3. Test Locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install from local directory
|
|
||||||
specify preset add --dev /path/to/your-preset
|
|
||||||
|
|
||||||
# Verify templates resolve from your preset
|
|
||||||
specify preset resolve spec-template
|
|
||||||
|
|
||||||
# Verify preset info
|
|
||||||
specify preset info your-preset
|
|
||||||
|
|
||||||
# List installed presets
|
|
||||||
specify preset list
|
|
||||||
|
|
||||||
# Remove when done testing
|
|
||||||
specify preset remove your-preset
|
|
||||||
```
|
|
||||||
|
|
||||||
If your preset includes command overrides, verify they appear in the agent directories:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check Claude commands (if using Claude)
|
|
||||||
ls .claude/commands/speckit.*.md
|
|
||||||
|
|
||||||
# Check Copilot commands (if using Copilot)
|
|
||||||
ls .github/agents/speckit.*.agent.md
|
|
||||||
|
|
||||||
# Check Gemini commands (if using Gemini)
|
|
||||||
ls .gemini/commands/speckit.*.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Create GitHub Release
|
|
||||||
|
|
||||||
Create a GitHub release for your preset version:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Tag the release
|
|
||||||
git tag v1.0.0
|
|
||||||
git push origin v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
The release archive URL will be:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Test Installation from Archive
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify preset add --from https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Submit to Catalog
|
|
||||||
|
|
||||||
### Understanding the Catalogs
|
|
||||||
|
|
||||||
Spec Kit uses a dual-catalog system:
|
|
||||||
|
|
||||||
- **`catalog.json`** — Official, verified presets (install allowed by default)
|
|
||||||
- **`catalog.community.json`** — Community-contributed presets (discovery only by default)
|
|
||||||
|
|
||||||
All community presets should be submitted to `catalog.community.json`.
|
|
||||||
|
|
||||||
### 1. Fork the spec-kit Repository
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/YOUR-USERNAME/spec-kit.git
|
|
||||||
cd spec-kit
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Add Preset to Community Catalog
|
|
||||||
|
|
||||||
Edit `presets/catalog.community.json` and add your preset.
|
|
||||||
|
|
||||||
> **⚠️ Entries must be sorted alphabetically by preset ID.** Insert your preset in the correct position within the `"presets"` object.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"schema_version": "1.0",
|
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
|
||||||
"presets": {
|
|
||||||
"your-preset": {
|
|
||||||
"name": "Your Preset Name",
|
|
||||||
"description": "Brief description of what your preset provides",
|
|
||||||
"author": "Your Name",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"download_url": "https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip",
|
|
||||||
"repository": "https://github.com/your-org/spec-kit-preset-your-preset",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"templates": 3,
|
|
||||||
"commands": 1
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"category",
|
|
||||||
"workflow"
|
|
||||||
],
|
|
||||||
"created_at": "2026-03-10T00:00:00Z",
|
|
||||||
"updated_at": "2026-03-10T00:00:00Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Submit Pull Request
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout -b add-your-preset
|
|
||||||
git add presets/catalog.community.json
|
|
||||||
git commit -m "Add your-preset to community catalog
|
|
||||||
|
|
||||||
- Preset ID: your-preset
|
|
||||||
- Version: 1.0.0
|
|
||||||
- Author: Your Name
|
|
||||||
- Description: Brief description
|
|
||||||
"
|
|
||||||
git push origin add-your-preset
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pull Request Checklist**:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
## Preset Submission
|
|
||||||
|
|
||||||
**Preset Name**: Your Preset Name
|
|
||||||
**Preset ID**: your-preset
|
|
||||||
**Version**: 1.0.0
|
|
||||||
**Repository**: https://github.com/your-org/spec-kit-preset-your-preset
|
|
||||||
|
|
||||||
### Checklist
|
|
||||||
- [ ] Valid preset.yml manifest
|
|
||||||
- [ ] README.md with description and usage
|
|
||||||
- [ ] LICENSE file included
|
|
||||||
- [ ] GitHub release created
|
|
||||||
- [ ] Preset tested with `specify preset add --dev`
|
|
||||||
- [ ] Templates resolve correctly (`specify preset resolve`)
|
|
||||||
- [ ] Commands register to agent directories (if applicable)
|
|
||||||
- [ ] Commands match template sections (command + template are coherent)
|
|
||||||
- [ ] Added to presets/catalog.community.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Process
|
|
||||||
|
|
||||||
After submission, maintainers will review:
|
|
||||||
|
|
||||||
1. **Manifest validation** — valid `preset.yml`, all files exist
|
|
||||||
2. **Template quality** — templates are useful and well-structured
|
|
||||||
3. **Command coherence** — commands reference sections that exist in templates
|
|
||||||
4. **Security** — no malicious content, safe file operations
|
|
||||||
5. **Documentation** — clear README explaining what the preset does
|
|
||||||
|
|
||||||
Once verified, `verified: true` is set and the preset appears in `specify preset search`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Release Workflow
|
|
||||||
|
|
||||||
When releasing a new version:
|
|
||||||
|
|
||||||
1. Update `version` in `preset.yml`
|
|
||||||
2. Update CHANGELOG.md
|
|
||||||
3. Tag and push: `git tag v1.1.0 && git push origin v1.1.0`
|
|
||||||
4. Submit PR to update `version` and `download_url` in `presets/catalog.community.json`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Template Design
|
|
||||||
|
|
||||||
- **Keep sections clear** — use headings and placeholder text the LLM can replace
|
|
||||||
- **Match commands to templates** — if your preset overrides a command, make sure it references the sections in your template
|
|
||||||
- **Document customization points** — use HTML comments to guide users on what to change
|
|
||||||
|
|
||||||
### Naming
|
|
||||||
|
|
||||||
- Preset IDs should be descriptive: `healthcare-compliance`, `enterprise-safe`, `startup-lean`
|
|
||||||
- Avoid generic names: `my-preset`, `custom`, `test`
|
|
||||||
|
|
||||||
### Stacking
|
|
||||||
|
|
||||||
- Design presets to work well when stacked with others
|
|
||||||
- Only override templates you need to change
|
|
||||||
- Document which templates and commands your preset modifies
|
|
||||||
|
|
||||||
### Command Overrides
|
|
||||||
|
|
||||||
- Only override commands when the workflow needs to change, not just the output format
|
|
||||||
- If you only need different template sections, a template override is sufficient
|
|
||||||
- Test command overrides with multiple agents (Claude, Gemini, Copilot)
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# Presets
|
|
||||||
|
|
||||||
Presets are stackable, priority-ordered collections of template and command overrides for Spec Kit. They let you customize both the artifacts produced by the Spec-Driven Development workflow (specs, plans, tasks, checklists, constitutions) and the commands that guide the LLM in creating them — without forking or modifying core files.
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
When Spec Kit needs a template (e.g. `spec-template`), it walks a resolution stack:
|
|
||||||
|
|
||||||
1. `.specify/templates/overrides/` — project-local one-off overrides
|
|
||||||
2. `.specify/presets/<preset-id>/templates/` — installed presets (sorted by priority)
|
|
||||||
3. `.specify/extensions/<ext-id>/templates/` — extension-provided templates
|
|
||||||
4. `.specify/templates/` — core templates shipped with Spec Kit
|
|
||||||
|
|
||||||
If no preset is installed, core templates are used — exactly the same behavior as before presets existed.
|
|
||||||
|
|
||||||
Template resolution happens **at runtime** — although preset files are copied into `.specify/presets/<id>/` during installation, Spec Kit walks the resolution stack on every template lookup rather than merging templates into a single location.
|
|
||||||
|
|
||||||
For detailed resolution and command registration flows, see [ARCHITECTURE.md](ARCHITECTURE.md).
|
|
||||||
|
|
||||||
## Command Overrides
|
|
||||||
|
|
||||||
Presets can also override the commands that guide the SDD workflow. Templates define *what* gets produced (specs, plans, constitutions); commands define *how* the LLM produces them (the step-by-step instructions).
|
|
||||||
|
|
||||||
Unlike templates, command overrides are applied **at install time**. When a preset includes `type: "command"` entries, the commands are registered into all detected agent directories (`.claude/commands/`, `.gemini/commands/`, etc.) in the correct format (Markdown or TOML with appropriate argument placeholders). When the preset is removed, the registered commands are cleaned up.
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Search available presets
|
|
||||||
specify preset search
|
|
||||||
|
|
||||||
# Install a preset from the catalog
|
|
||||||
specify preset add healthcare-compliance
|
|
||||||
|
|
||||||
# Install from a local directory (for development)
|
|
||||||
specify preset add --dev ./my-preset
|
|
||||||
|
|
||||||
# Install with a specific priority (lower = higher precedence)
|
|
||||||
specify preset add healthcare-compliance --priority 5
|
|
||||||
|
|
||||||
# List installed presets
|
|
||||||
specify preset list
|
|
||||||
|
|
||||||
# See which template a name resolves to
|
|
||||||
specify preset resolve spec-template
|
|
||||||
|
|
||||||
# Get detailed info about a preset
|
|
||||||
specify preset info healthcare-compliance
|
|
||||||
|
|
||||||
# Remove a preset
|
|
||||||
specify preset remove healthcare-compliance
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stacking Presets
|
|
||||||
|
|
||||||
Multiple presets can be installed simultaneously. The `--priority` flag controls which one wins when two presets provide the same template (lower number = higher precedence):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
specify preset add enterprise-safe --priority 10 # base layer
|
|
||||||
specify preset add healthcare-compliance --priority 5 # overrides enterprise-safe
|
|
||||||
specify preset add pm-workflow --priority 1 # overrides everything
|
|
||||||
```
|
|
||||||
|
|
||||||
Presets **override**, they don't merge. If two presets both provide `spec-template`, the one with the lowest priority number wins entirely.
|
|
||||||
|
|
||||||
## Catalog Management
|
|
||||||
|
|
||||||
Presets are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List active catalogs
|
|
||||||
specify preset catalog list
|
|
||||||
|
|
||||||
# Add a custom catalog
|
|
||||||
specify preset catalog add https://example.com/catalog.json --name my-org --install-allowed
|
|
||||||
|
|
||||||
# Remove a catalog
|
|
||||||
specify preset catalog remove my-org
|
|
||||||
```
|
|
||||||
|
|
||||||
## Creating a Preset
|
|
||||||
|
|
||||||
See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset.
|
|
||||||
|
|
||||||
1. Copy `scaffold/` to a new directory
|
|
||||||
2. Edit `preset.yml` with your preset's metadata
|
|
||||||
3. Add or replace templates in `templates/`
|
|
||||||
4. Test locally with `specify preset add --dev .`
|
|
||||||
5. Verify with `specify preset resolve spec-template`
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `SPECKIT_PRESET_CATALOG_URL` | Override the catalog URL (replaces all defaults) |
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
| File | Scope | Description |
|
|
||||||
|------|-------|-------------|
|
|
||||||
| `.specify/preset-catalogs.yml` | Project | Custom catalog stack for this project |
|
|
||||||
| `~/.specify/preset-catalogs.yml` | User | Custom catalog stack for all projects |
|
|
||||||
|
|
||||||
## Future Considerations
|
|
||||||
|
|
||||||
The following enhancements are under consideration for future releases:
|
|
||||||
|
|
||||||
- **Composition strategies** — Allow presets to declare a `strategy` per template instead of the default `replace`:
|
|
||||||
|
|
||||||
| Type | `replace` | `prepend` | `append` | `wrap` |
|
|
||||||
|------|-----------|-----------|----------|--------|
|
|
||||||
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
|
|
||||||
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
|
|
||||||
| **script** | ✓ (default) | — | — | ✓ |
|
|
||||||
|
|
||||||
For artifacts and commands (which are LLM directives), `wrap` would inject preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder. For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable.
|
|
||||||
- **Script overrides** — Enable presets to provide alternative versions of core scripts (e.g. `create-new-feature.sh`) for workflow customization. A `strategy: "wrap"` option could allow presets to run custom logic before/after the core script without fully replacing it.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "1.0",
|
|
||||||
"updated_at": "2026-03-24T00:00:00Z",
|
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
|
||||||
"presets": {
|
|
||||||
"aide-in-place": {
|
|
||||||
"name": "AIDE In-Place Migration",
|
|
||||||
"id": "aide-in-place",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Adapts the AIDE workflow for in-place technology migrations (X → Y pattern). Overrides vision, roadmap, progress, and work item commands with migration-specific guidance.",
|
|
||||||
"author": "mnriem",
|
|
||||||
"repository": "https://github.com/mnriem/spec-kit-presets",
|
|
||||||
"download_url": "https://github.com/mnriem/spec-kit-presets/releases/download/aide-in-place-v1.0.0/aide-in-place.zip",
|
|
||||||
"homepage": "https://github.com/mnriem/spec-kit-presets",
|
|
||||||
"documentation": "https://github.com/mnriem/spec-kit-presets/blob/main/aide-in-place/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.2.0",
|
|
||||||
"extensions": ["aide"]
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"templates": 2,
|
|
||||||
"commands": 8
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"migration",
|
|
||||||
"in-place",
|
|
||||||
"brownfield",
|
|
||||||
"aide"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pirate": {
|
|
||||||
"name": "Pirate Speak (Full)",
|
|
||||||
"id": "pirate",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Arrr! Transforms all Spec Kit output into pirate speak. Specs, plans, and tasks be written fer scallywags.",
|
|
||||||
"author": "mnriem",
|
|
||||||
"repository": "https://github.com/mnriem/spec-kit-presets",
|
|
||||||
"download_url": "https://github.com/mnriem/spec-kit-presets/releases/download/pirate-v1.0.0/pirate.zip",
|
|
||||||
"homepage": "https://github.com/mnriem/spec-kit-presets",
|
|
||||||
"documentation": "https://github.com/mnriem/spec-kit-presets/blob/main/pirate/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"requires": {
|
|
||||||
"speckit_version": ">=0.1.0"
|
|
||||||
},
|
|
||||||
"provides": {
|
|
||||||
"templates": 6,
|
|
||||||
"commands": 9
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
"pirate",
|
|
||||||
"theme",
|
|
||||||
"fun",
|
|
||||||
"experimental"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"schema_version": "1.0",
|
|
||||||
"updated_at": "2026-03-10T00:00:00Z",
|
|
||||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
|
|
||||||
"presets": {}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# My Preset
|
|
||||||
|
|
||||||
A custom preset for Spec Kit. Copy this directory and customize it to create your own.
|
|
||||||
|
|
||||||
## Templates Included
|
|
||||||
|
|
||||||
| Template | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `spec-template` | template | Custom feature specification template (overrides core and extensions) |
|
|
||||||
| `myext-template` | template | Override of the myext extension's report template |
|
|
||||||
| `speckit.specify` | command | Custom specification command (overrides core) |
|
|
||||||
| `speckit.myext.myextcmd` | command | Override of the myext extension's myextcmd command |
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
1. Copy this directory: `cp -r presets/scaffold my-preset`
|
|
||||||
2. Edit `preset.yml` — set your preset's ID, name, description, and templates
|
|
||||||
3. Add or modify templates in `templates/`
|
|
||||||
4. Test locally: `specify preset add --dev ./my-preset`
|
|
||||||
5. Verify resolution: `specify preset resolve spec-template`
|
|
||||||
6. Remove when done testing: `specify preset remove my-preset`
|
|
||||||
|
|
||||||
## Manifest Reference (`preset.yml`)
|
|
||||||
|
|
||||||
Required fields:
|
|
||||||
- `schema_version` — always `"1.0"`
|
|
||||||
- `preset.id` — lowercase alphanumeric with hyphens
|
|
||||||
- `preset.name` — human-readable name
|
|
||||||
- `preset.version` — semantic version (e.g. `1.0.0`)
|
|
||||||
- `preset.description` — brief description
|
|
||||||
- `requires.speckit_version` — version constraint (e.g. `>=0.1.0`)
|
|
||||||
- `provides.templates` — list of templates with `type`, `name`, and `file`
|
|
||||||
|
|
||||||
## Template Types
|
|
||||||
|
|
||||||
- **template** — Document scaffolds (spec-template.md, plan-template.md, tasks-template.md, etc.)
|
|
||||||
- **command** — AI agent workflow prompts (e.g. speckit.specify, speckit.plan)
|
|
||||||
- **script** — Custom scripts (reserved for future use)
|
|
||||||
|
|
||||||
## Publishing
|
|
||||||
|
|
||||||
See the [Preset Publishing Guide](../PUBLISHING.md) for details on submitting to the catalog.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Override of the myext extension's myextcmd command"
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Preset override for speckit.myext.myextcmd -->
|
|
||||||
|
|
||||||
You are following a customized version of the myext extension's myextcmd command.
|
|
||||||
|
|
||||||
When executing this command:
|
|
||||||
|
|
||||||
1. Read the user's input from $ARGUMENTS
|
|
||||||
2. Follow the standard myextcmd workflow
|
|
||||||
3. Additionally, apply the following customizations from this preset:
|
|
||||||
- Add compliance checks before proceeding
|
|
||||||
- Include audit trail entries in the output
|
|
||||||
|
|
||||||
> CUSTOMIZE: Replace the instructions above with your own.
|
|
||||||
> This file overrides the command that the "myext" extension provides.
|
|
||||||
> When this preset is installed, all agents (Claude, Gemini, Copilot, etc.)
|
|
||||||
> will use this version instead of the extension's original.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Create a feature specification (preset override)"
|
|
||||||
scripts:
|
|
||||||
sh: scripts/bash/create-new-feature.sh "{ARGS}"
|
|
||||||
ps: scripts/powershell/create-new-feature.ps1 "{ARGS}"
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Input
|
|
||||||
|
|
||||||
```text
|
|
||||||
$ARGUMENTS
|
|
||||||
```
|
|
||||||
|
|
||||||
Given the feature description above:
|
|
||||||
|
|
||||||
1. **Create the feature branch** by running the script:
|
|
||||||
- Bash: `{SCRIPT} --json --short-name "<short-name>" "<description>"`
|
|
||||||
- The JSON output contains BRANCH_NAME and SPEC_FILE paths.
|
|
||||||
|
|
||||||
2. **Read the spec-template** to see the sections you need to fill.
|
|
||||||
|
|
||||||
3. **Write the specification** to SPEC_FILE, replacing the placeholders in each section
|
|
||||||
(Overview, Requirements, Acceptance Criteria) with details from the user's description.
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
schema_version: "1.0"
|
|
||||||
|
|
||||||
preset:
|
|
||||||
# CUSTOMIZE: Change 'my-preset' to your preset ID (lowercase, hyphen-separated)
|
|
||||||
id: "my-preset"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Human-readable name for your preset
|
|
||||||
name: "My Preset"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Update version when releasing (semantic versioning: X.Y.Z)
|
|
||||||
version: "1.0.0"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Brief description (under 200 characters)
|
|
||||||
description: "Brief description of what your preset provides"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Your name or organization name
|
|
||||||
author: "Your Name"
|
|
||||||
|
|
||||||
# CUSTOMIZE: GitHub repository URL (create before publishing)
|
|
||||||
repository: "https://github.com/your-org/spec-kit-preset-my-preset"
|
|
||||||
|
|
||||||
# REVIEW: License (MIT is recommended for open source)
|
|
||||||
license: "MIT"
|
|
||||||
|
|
||||||
# Requirements for this preset
|
|
||||||
requires:
|
|
||||||
# CUSTOMIZE: Minimum spec-kit version required
|
|
||||||
speckit_version: ">=0.1.0"
|
|
||||||
|
|
||||||
# Templates provided by this preset
|
|
||||||
provides:
|
|
||||||
templates:
|
|
||||||
# CUSTOMIZE: Define your template overrides
|
|
||||||
# Templates are document scaffolds (spec-template.md, plan-template.md, etc.)
|
|
||||||
- type: "template"
|
|
||||||
name: "spec-template"
|
|
||||||
file: "templates/spec-template.md"
|
|
||||||
description: "Custom feature specification template"
|
|
||||||
replaces: "spec-template" # Which core template this overrides (optional)
|
|
||||||
|
|
||||||
# ADD MORE TEMPLATES: Copy this block for each template
|
|
||||||
# - type: "template"
|
|
||||||
# name: "plan-template"
|
|
||||||
# file: "templates/plan-template.md"
|
|
||||||
# description: "Custom plan template"
|
|
||||||
# replaces: "plan-template"
|
|
||||||
|
|
||||||
# OVERRIDE EXTENSION TEMPLATES:
|
|
||||||
# Presets sit above extensions in the resolution stack, so you can
|
|
||||||
# override templates provided by any installed extension.
|
|
||||||
# For example, if the "myext" extension provides a spec-template,
|
|
||||||
# the preset's version above will take priority automatically.
|
|
||||||
|
|
||||||
# Override a template provided by the "myext" extension:
|
|
||||||
- type: "template"
|
|
||||||
name: "myext-template"
|
|
||||||
file: "templates/myext-template.md"
|
|
||||||
description: "Override myext's report template"
|
|
||||||
replaces: "myext-template"
|
|
||||||
|
|
||||||
# Command overrides (AI agent workflow prompts)
|
|
||||||
# Presets can override both core and extension commands.
|
|
||||||
# Commands are automatically registered into all detected agent
|
|
||||||
# directories (.claude/commands/, .gemini/commands/, etc.)
|
|
||||||
|
|
||||||
# Override a core command:
|
|
||||||
- type: "command"
|
|
||||||
name: "speckit.specify"
|
|
||||||
file: "commands/speckit.specify.md"
|
|
||||||
description: "Custom specification command"
|
|
||||||
replaces: "speckit.specify"
|
|
||||||
|
|
||||||
# Override an extension command (e.g. from the "myext" extension):
|
|
||||||
- type: "command"
|
|
||||||
name: "speckit.myext.myextcmd"
|
|
||||||
file: "commands/speckit.myext.myextcmd.md"
|
|
||||||
description: "Override myext's myextcmd command with custom workflow"
|
|
||||||
replaces: "speckit.myext.myextcmd"
|
|
||||||
|
|
||||||
# Script templates (reserved for future use)
|
|
||||||
# - type: "script"
|
|
||||||
# name: "create-new-feature"
|
|
||||||
# file: "scripts/bash/create-new-feature.sh"
|
|
||||||
# description: "Custom feature creation script"
|
|
||||||
# replaces: "create-new-feature"
|
|
||||||
|
|
||||||
# CUSTOMIZE: Add relevant tags (2-5 recommended)
|
|
||||||
# Used for discovery in catalog
|
|
||||||
tags:
|
|
||||||
- "example"
|
|
||||||
- "preset"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# MyExt Report
|
|
||||||
|
|
||||||
> This template overrides the one provided by the "myext" extension.
|
|
||||||
> Customize it to match your needs.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
Brief summary of the report.
|
|
||||||
|
|
||||||
## Details
|
|
||||||
|
|
||||||
- Detail 1
|
|
||||||
- Detail 2
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
- [ ] Action 1
|
|
||||||
- [ ] Action 2
|
|
||||||
|
|
||||||
<!--
|
|
||||||
CUSTOMIZE: This template takes priority over the myext extension's
|
|
||||||
version of myext-template. The extension's original is still available
|
|
||||||
if you remove this preset.
|
|
||||||
-->
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Feature Specification: [FEATURE NAME]
|
|
||||||
|
|
||||||
**Created**: [DATE]
|
|
||||||
**Status**: Draft
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
[Brief description of the feature]
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- [ ] Requirement 1
|
|
||||||
- [ ] Requirement 2
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [ ] Criterion 1
|
|
||||||
- [ ] Criterion 2
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
---
|
|
||||||
description: "Self-test override of the specify command"
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
You are following the self-test preset's version of the specify command.
|
|
||||||
|
|
||||||
When creating a specification, follow this process:
|
|
||||||
|
|
||||||
1. Read the user's requirements from $ARGUMENTS
|
|
||||||
2. Create a specification document using the spec-template
|
|
||||||
3. Include all standard sections plus the self-test marker
|
|
||||||
|
|
||||||
> This command is provided by the self-test preset.
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
schema_version: "1.0"
|
|
||||||
|
|
||||||
preset:
|
|
||||||
id: "self-test"
|
|
||||||
name: "Self-Test Preset"
|
|
||||||
version: "1.0.0"
|
|
||||||
description: "A preset that overrides all core templates for testing purposes"
|
|
||||||
author: "github"
|
|
||||||
repository: "https://github.com/github/spec-kit"
|
|
||||||
license: "MIT"
|
|
||||||
|
|
||||||
requires:
|
|
||||||
speckit_version: ">=0.1.0"
|
|
||||||
|
|
||||||
provides:
|
|
||||||
templates:
|
|
||||||
- type: "template"
|
|
||||||
name: "spec-template"
|
|
||||||
file: "templates/spec-template.md"
|
|
||||||
description: "Self-test spec template"
|
|
||||||
replaces: "spec-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "plan-template"
|
|
||||||
file: "templates/plan-template.md"
|
|
||||||
description: "Self-test plan template"
|
|
||||||
replaces: "plan-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "tasks-template"
|
|
||||||
file: "templates/tasks-template.md"
|
|
||||||
description: "Self-test tasks template"
|
|
||||||
replaces: "tasks-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "checklist-template"
|
|
||||||
file: "templates/checklist-template.md"
|
|
||||||
description: "Self-test checklist template"
|
|
||||||
replaces: "checklist-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "constitution-template"
|
|
||||||
file: "templates/constitution-template.md"
|
|
||||||
description: "Self-test constitution template"
|
|
||||||
replaces: "constitution-template"
|
|
||||||
|
|
||||||
- type: "template"
|
|
||||||
name: "agent-file-template"
|
|
||||||
file: "templates/agent-file-template.md"
|
|
||||||
description: "Self-test agent file template"
|
|
||||||
replaces: "agent-file-template"
|
|
||||||
|
|
||||||
- type: "command"
|
|
||||||
name: "speckit.specify"
|
|
||||||
file: "commands/speckit.specify.md"
|
|
||||||
description: "Self-test override of the specify command"
|
|
||||||
replaces: "speckit.specify"
|
|
||||||
|
|
||||||
tags:
|
|
||||||
- "testing"
|
|
||||||
- "self-test"
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Agent File (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Agent Instructions
|
|
||||||
|
|
||||||
Follow these guidelines when working on this project.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Checklist (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Pre-Implementation
|
|
||||||
|
|
||||||
- [ ] Spec reviewed
|
|
||||||
- [ ] Plan approved
|
|
||||||
|
|
||||||
## Post-Implementation
|
|
||||||
|
|
||||||
- [ ] Tests passing
|
|
||||||
- [ ] Documentation updated
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Constitution (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Principles
|
|
||||||
|
|
||||||
1. Principle 1
|
|
||||||
2. Principle 2
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
|
|
||||||
- Guideline 1
|
|
||||||
- Guideline 2
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Implementation Plan (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Approach
|
|
||||||
|
|
||||||
Describe the implementation approach.
|
|
||||||
|
|
||||||
## Steps
|
|
||||||
|
|
||||||
1. Step 1
|
|
||||||
2. Step 2
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- Dependency 1
|
|
||||||
|
|
||||||
## Risks
|
|
||||||
|
|
||||||
- Risk 1
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Feature Specification (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Brief description of the feature.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Requirement 1
|
|
||||||
- Requirement 2
|
|
||||||
|
|
||||||
## Design
|
|
||||||
|
|
||||||
Describe the design approach.
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
- [ ] Criterion 1
|
|
||||||
- [ ] Criterion 2
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# Tasks (Self-Test Preset)
|
|
||||||
|
|
||||||
<!-- preset:self-test -->
|
|
||||||
|
|
||||||
> This template is provided by the self-test preset.
|
|
||||||
|
|
||||||
## Task List
|
|
||||||
|
|
||||||
- [ ] Task 1
|
|
||||||
- [ ] Task 2
|
|
||||||
|
|
||||||
## Estimation
|
|
||||||
|
|
||||||
| Task | Estimate |
|
|
||||||
|------|----------|
|
|
||||||
| Task 1 | TBD |
|
|
||||||
| Task 2 | TBD |
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "specify-cli"
|
name = "specify-cli"
|
||||||
version = "0.4.6.dev0"
|
version = "0.1.13"
|
||||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -13,8 +13,6 @@ dependencies = [
|
|||||||
"truststore>=0.10.4",
|
"truststore>=0.10.4",
|
||||||
"pyyaml>=6.0",
|
"pyyaml>=6.0",
|
||||||
"packaging>=23.0",
|
"packaging>=23.0",
|
||||||
"pathspec>=0.12.0",
|
|
||||||
"json5>=0.13.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -27,21 +25,6 @@ build-backend = "hatchling.build"
|
|||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/specify_cli"]
|
packages = ["src/specify_cli"]
|
||||||
|
|
||||||
[tool.hatch.build.targets.wheel.force-include]
|
|
||||||
# Bundle core assets so `specify init` works without network access (air-gapped / enterprise)
|
|
||||||
# Page templates (exclude commands/ — bundled separately below to avoid duplication)
|
|
||||||
"templates/agent-file-template.md" = "specify_cli/core_pack/templates/agent-file-template.md"
|
|
||||||
"templates/checklist-template.md" = "specify_cli/core_pack/templates/checklist-template.md"
|
|
||||||
"templates/constitution-template.md" = "specify_cli/core_pack/templates/constitution-template.md"
|
|
||||||
"templates/plan-template.md" = "specify_cli/core_pack/templates/plan-template.md"
|
|
||||||
"templates/spec-template.md" = "specify_cli/core_pack/templates/spec-template.md"
|
|
||||||
"templates/tasks-template.md" = "specify_cli/core_pack/templates/tasks-template.md"
|
|
||||||
"templates/vscode-settings.json" = "specify_cli/core_pack/templates/vscode-settings.json"
|
|
||||||
# Command templates
|
|
||||||
"templates/commands" = "specify_cli/core_pack/commands"
|
|
||||||
"scripts/bash" = "specify_cli/core_pack/scripts/bash"
|
|
||||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
test = [
|
test = [
|
||||||
"pytest>=7.0",
|
"pytest>=7.0",
|
||||||
|
|||||||
@@ -79,28 +79,15 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/common.sh"
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
# Get feature paths and validate branch
|
# Get feature paths and validate branch
|
||||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
eval $(get_feature_paths)
|
||||||
eval "$_paths_output"
|
|
||||||
unset _paths_output
|
|
||||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||||
|
|
||||||
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
# If paths-only mode, output paths and exit (support JSON + paths-only combined)
|
||||||
if $PATHS_ONLY; then
|
if $PATHS_ONLY; then
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
# Minimal JSON paths payload (no validation performed)
|
# Minimal JSON paths payload (no validation performed)
|
||||||
if has_jq; then
|
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
||||||
jq -cn \
|
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
|
||||||
--arg repo_root "$REPO_ROOT" \
|
|
||||||
--arg branch "$CURRENT_BRANCH" \
|
|
||||||
--arg feature_dir "$FEATURE_DIR" \
|
|
||||||
--arg feature_spec "$FEATURE_SPEC" \
|
|
||||||
--arg impl_plan "$IMPL_PLAN" \
|
|
||||||
--arg tasks "$TASKS" \
|
|
||||||
'{REPO_ROOT:$repo_root,BRANCH:$branch,FEATURE_DIR:$feature_dir,FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,TASKS:$tasks}'
|
|
||||||
else
|
|
||||||
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
|
|
||||||
"$(json_escape "$REPO_ROOT")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$TASKS")"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "REPO_ROOT: $REPO_ROOT"
|
echo "REPO_ROOT: $REPO_ROOT"
|
||||||
echo "BRANCH: $CURRENT_BRANCH"
|
echo "BRANCH: $CURRENT_BRANCH"
|
||||||
@@ -154,25 +141,14 @@ fi
|
|||||||
# Output results
|
# Output results
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
# Build JSON array of documents
|
# Build JSON array of documents
|
||||||
if has_jq; then
|
if [[ ${#docs[@]} -eq 0 ]]; then
|
||||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
json_docs="[]"
|
||||||
json_docs="[]"
|
|
||||||
else
|
|
||||||
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
|
|
||||||
fi
|
|
||||||
jq -cn \
|
|
||||||
--arg feature_dir "$FEATURE_DIR" \
|
|
||||||
--argjson docs "$json_docs" \
|
|
||||||
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs}'
|
|
||||||
else
|
else
|
||||||
if [[ ${#docs[@]} -eq 0 ]]; then
|
json_docs=$(printf '"%s",' "${docs[@]}")
|
||||||
json_docs="[]"
|
json_docs="[${json_docs%,}]"
|
||||||
else
|
|
||||||
json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
|
|
||||||
json_docs="[${json_docs%,}]"
|
|
||||||
fi
|
|
||||||
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$(json_escape "$FEATURE_DIR")" "$json_docs"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
|
||||||
else
|
else
|
||||||
# Text output
|
# Text output
|
||||||
echo "FEATURE_DIR:$FEATURE_DIR"
|
echo "FEATURE_DIR:$FEATURE_DIR"
|
||||||
|
|||||||
@@ -1,48 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Common functions and variables for all scripts
|
# Common functions and variables for all scripts
|
||||||
|
|
||||||
# Find repository root by searching upward for .specify directory
|
# Get repository root, with fallback for non-git repositories
|
||||||
# This is the primary marker for spec-kit projects
|
|
||||||
find_specify_root() {
|
|
||||||
local dir="${1:-$(pwd)}"
|
|
||||||
# Normalize to absolute path to prevent infinite loop with relative paths
|
|
||||||
# Use -- to handle paths starting with - (e.g., -P, -L)
|
|
||||||
dir="$(cd -- "$dir" 2>/dev/null && pwd)" || return 1
|
|
||||||
local prev_dir=""
|
|
||||||
while true; do
|
|
||||||
if [ -d "$dir/.specify" ]; then
|
|
||||||
echo "$dir"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
# Stop if we've reached filesystem root or dirname stops changing
|
|
||||||
if [ "$dir" = "/" ] || [ "$dir" = "$prev_dir" ]; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
prev_dir="$dir"
|
|
||||||
dir="$(dirname "$dir")"
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get repository root, prioritizing .specify directory over git
|
|
||||||
# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
|
|
||||||
get_repo_root() {
|
get_repo_root() {
|
||||||
# First, look for .specify directory (spec-kit's own marker)
|
|
||||||
local specify_root
|
|
||||||
if specify_root=$(find_specify_root); then
|
|
||||||
echo "$specify_root"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fallback to git if no .specify found
|
|
||||||
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
git rev-parse --show-toplevel
|
git rev-parse --show-toplevel
|
||||||
return
|
else
|
||||||
|
# Fall back to script location for non-git repos
|
||||||
|
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
(cd "$script_dir/../../.." && pwd)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Final fallback to script location for non-git repos
|
|
||||||
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
(cd "$script_dir/../../.." && pwd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get current branch, with fallback for non-git repositories
|
# Get current branch, with fallback for non-git repositories
|
||||||
@@ -53,40 +20,29 @@ get_current_branch() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Then check git if available at the spec-kit root (not parent)
|
# Then check git if available
|
||||||
local repo_root=$(get_repo_root)
|
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
|
||||||
if has_git; then
|
git rev-parse --abbrev-ref HEAD
|
||||||
git -C "$repo_root" rev-parse --abbrev-ref HEAD
|
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For non-git repos, try to find the latest feature directory
|
# For non-git repos, try to find the latest feature directory
|
||||||
|
local repo_root=$(get_repo_root)
|
||||||
local specs_dir="$repo_root/specs"
|
local specs_dir="$repo_root/specs"
|
||||||
|
|
||||||
if [[ -d "$specs_dir" ]]; then
|
if [[ -d "$specs_dir" ]]; then
|
||||||
local latest_feature=""
|
local latest_feature=""
|
||||||
local highest=0
|
local highest=0
|
||||||
local latest_timestamp=""
|
|
||||||
|
|
||||||
for dir in "$specs_dir"/*; do
|
for dir in "$specs_dir"/*; do
|
||||||
if [[ -d "$dir" ]]; then
|
if [[ -d "$dir" ]]; then
|
||||||
local dirname=$(basename "$dir")
|
local dirname=$(basename "$dir")
|
||||||
if [[ "$dirname" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
|
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
|
||||||
# Timestamp-based branch: compare lexicographically
|
|
||||||
local ts="${BASH_REMATCH[1]}"
|
|
||||||
if [[ "$ts" > "$latest_timestamp" ]]; then
|
|
||||||
latest_timestamp="$ts"
|
|
||||||
latest_feature=$dirname
|
|
||||||
fi
|
|
||||||
elif [[ "$dirname" =~ ^([0-9]{3,})- ]]; then
|
|
||||||
local number=${BASH_REMATCH[1]}
|
local number=${BASH_REMATCH[1]}
|
||||||
number=$((10#$number))
|
number=$((10#$number))
|
||||||
if [[ "$number" -gt "$highest" ]]; then
|
if [[ "$number" -gt "$highest" ]]; then
|
||||||
highest=$number
|
highest=$number
|
||||||
# Only update if no timestamp branch found yet
|
latest_feature=$dirname
|
||||||
if [[ -z "$latest_timestamp" ]]; then
|
|
||||||
latest_feature=$dirname
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -101,17 +57,9 @@ get_current_branch() {
|
|||||||
echo "main" # Final fallback
|
echo "main" # Final fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if we have git available at the spec-kit root level
|
# Check if we have git available
|
||||||
# Returns true only if git is installed and the repo root is inside a git work tree
|
|
||||||
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
|
|
||||||
has_git() {
|
has_git() {
|
||||||
# First check if git command is available (before calling get_repo_root which may use git)
|
git rev-parse --show-toplevel >/dev/null 2>&1
|
||||||
command -v git >/dev/null 2>&1 || return 1
|
|
||||||
local repo_root=$(get_repo_root)
|
|
||||||
# Check if .git exists (directory or file for worktrees/submodules)
|
|
||||||
[ -e "$repo_root/.git" ] || return 1
|
|
||||||
# Verify it's actually a valid git work tree
|
|
||||||
git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_feature_branch() {
|
check_feature_branch() {
|
||||||
@@ -124,15 +72,9 @@ check_feature_branch() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Accept sequential prefix (3+ digits) but exclude malformed timestamps
|
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
|
||||||
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
|
|
||||||
local is_sequential=false
|
|
||||||
if [[ "$branch" =~ ^[0-9]{3,}- ]] && [[ ! "$branch" =~ ^[0-9]{7}-[0-9]{6}- ]] && [[ ! "$branch" =~ ^[0-9]{7,8}-[0-9]{6}$ ]]; then
|
|
||||||
is_sequential=true
|
|
||||||
fi
|
|
||||||
if [[ "$is_sequential" != "true" ]] && [[ ! "$branch" =~ ^[0-9]{8}-[0-9]{6}- ]]; then
|
|
||||||
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
|
||||||
echo "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name" >&2
|
echo "Feature branches should be named like: 001-feature-name" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -148,18 +90,15 @@ find_feature_dir_by_prefix() {
|
|||||||
local branch_name="$2"
|
local branch_name="$2"
|
||||||
local specs_dir="$repo_root/specs"
|
local specs_dir="$repo_root/specs"
|
||||||
|
|
||||||
# Extract prefix from branch (e.g., "004" from "004-whatever" or "20260319-143022" from timestamp branches)
|
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
|
||||||
local prefix=""
|
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
|
||||||
if [[ "$branch_name" =~ ^([0-9]{8}-[0-9]{6})- ]]; then
|
# If branch doesn't have numeric prefix, fall back to exact match
|
||||||
prefix="${BASH_REMATCH[1]}"
|
|
||||||
elif [[ "$branch_name" =~ ^([0-9]{3,})- ]]; then
|
|
||||||
prefix="${BASH_REMATCH[1]}"
|
|
||||||
else
|
|
||||||
# If branch doesn't have a recognized prefix, fall back to exact match
|
|
||||||
echo "$specs_dir/$branch_name"
|
echo "$specs_dir/$branch_name"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
local prefix="${BASH_REMATCH[1]}"
|
||||||
|
|
||||||
# Search for directories in specs/ that start with this prefix
|
# Search for directories in specs/ that start with this prefix
|
||||||
local matches=()
|
local matches=()
|
||||||
if [[ -d "$specs_dir" ]]; then
|
if [[ -d "$specs_dir" ]]; then
|
||||||
@@ -180,8 +119,8 @@ find_feature_dir_by_prefix() {
|
|||||||
else
|
else
|
||||||
# Multiple matches - this shouldn't happen with proper naming convention
|
# Multiple matches - this shouldn't happen with proper naming convention
|
||||||
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
||||||
echo "Please ensure only one spec directory exists per prefix." >&2
|
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
||||||
return 1
|
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,142 +134,23 @@ get_feature_paths() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Use prefix-based lookup to support multiple branches per spec
|
# Use prefix-based lookup to support multiple branches per spec
|
||||||
local feature_dir
|
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
||||||
if ! feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch"); then
|
|
||||||
echo "ERROR: Failed to resolve feature directory" >&2
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use printf '%q' to safely quote values, preventing shell injection
|
cat <<EOF
|
||||||
# via crafted branch names or paths containing special characters
|
REPO_ROOT='$repo_root'
|
||||||
printf 'REPO_ROOT=%q\n' "$repo_root"
|
CURRENT_BRANCH='$current_branch'
|
||||||
printf 'CURRENT_BRANCH=%q\n' "$current_branch"
|
HAS_GIT='$has_git_repo'
|
||||||
printf 'HAS_GIT=%q\n' "$has_git_repo"
|
FEATURE_DIR='$feature_dir'
|
||||||
printf 'FEATURE_DIR=%q\n' "$feature_dir"
|
FEATURE_SPEC='$feature_dir/spec.md'
|
||||||
printf 'FEATURE_SPEC=%q\n' "$feature_dir/spec.md"
|
IMPL_PLAN='$feature_dir/plan.md'
|
||||||
printf 'IMPL_PLAN=%q\n' "$feature_dir/plan.md"
|
TASKS='$feature_dir/tasks.md'
|
||||||
printf 'TASKS=%q\n' "$feature_dir/tasks.md"
|
RESEARCH='$feature_dir/research.md'
|
||||||
printf 'RESEARCH=%q\n' "$feature_dir/research.md"
|
DATA_MODEL='$feature_dir/data-model.md'
|
||||||
printf 'DATA_MODEL=%q\n' "$feature_dir/data-model.md"
|
QUICKSTART='$feature_dir/quickstart.md'
|
||||||
printf 'QUICKSTART=%q\n' "$feature_dir/quickstart.md"
|
CONTRACTS_DIR='$feature_dir/contracts'
|
||||||
printf 'CONTRACTS_DIR=%q\n' "$feature_dir/contracts"
|
EOF
|
||||||
}
|
|
||||||
|
|
||||||
# Check if jq is available for safe JSON construction
|
|
||||||
has_jq() {
|
|
||||||
command -v jq >/dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Escape a string for safe embedding in a JSON value (fallback when jq is unavailable).
|
|
||||||
# Handles backslash, double-quote, and JSON-required control character escapes (RFC 8259).
|
|
||||||
json_escape() {
|
|
||||||
local s="$1"
|
|
||||||
s="${s//\\/\\\\}"
|
|
||||||
s="${s//\"/\\\"}"
|
|
||||||
s="${s//$'\n'/\\n}"
|
|
||||||
s="${s//$'\t'/\\t}"
|
|
||||||
s="${s//$'\r'/\\r}"
|
|
||||||
s="${s//$'\b'/\\b}"
|
|
||||||
s="${s//$'\f'/\\f}"
|
|
||||||
# Escape any remaining U+0001-U+001F control characters as \uXXXX.
|
|
||||||
# (U+0000/NUL cannot appear in bash strings and is excluded.)
|
|
||||||
# LC_ALL=C ensures ${#s} counts bytes and ${s:$i:1} yields single bytes,
|
|
||||||
# so multi-byte UTF-8 sequences (first byte >= 0xC0) pass through intact.
|
|
||||||
local LC_ALL=C
|
|
||||||
local i char code
|
|
||||||
for (( i=0; i<${#s}; i++ )); do
|
|
||||||
char="${s:$i:1}"
|
|
||||||
printf -v code '%d' "'$char" 2>/dev/null || code=256
|
|
||||||
if (( code >= 1 && code <= 31 )); then
|
|
||||||
printf '\\u%04x' "$code"
|
|
||||||
else
|
|
||||||
printf '%s' "$char"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
}
|
||||||
|
|
||||||
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
||||||
|
|
||||||
# Resolve a template name to a file path using the priority stack:
|
|
||||||
# 1. .specify/templates/overrides/
|
|
||||||
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
|
|
||||||
# 3. .specify/extensions/<ext-id>/templates/
|
|
||||||
# 4. .specify/templates/ (core)
|
|
||||||
resolve_template() {
|
|
||||||
local template_name="$1"
|
|
||||||
local repo_root="$2"
|
|
||||||
local base="$repo_root/.specify/templates"
|
|
||||||
|
|
||||||
# Priority 1: Project overrides
|
|
||||||
local override="$base/overrides/${template_name}.md"
|
|
||||||
[ -f "$override" ] && echo "$override" && return 0
|
|
||||||
|
|
||||||
# Priority 2: Installed presets (sorted by priority from .registry)
|
|
||||||
local presets_dir="$repo_root/.specify/presets"
|
|
||||||
if [ -d "$presets_dir" ]; then
|
|
||||||
local registry_file="$presets_dir/.registry"
|
|
||||||
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
|
|
||||||
# Read preset IDs sorted by priority (lower number = higher precedence).
|
|
||||||
# The python3 call is wrapped in an if-condition so that set -e does not
|
|
||||||
# abort the function when python3 exits non-zero (e.g. invalid JSON).
|
|
||||||
local sorted_presets=""
|
|
||||||
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
|
|
||||||
import json, sys, os
|
|
||||||
try:
|
|
||||||
with open(os.environ['SPECKIT_REGISTRY']) as f:
|
|
||||||
data = json.load(f)
|
|
||||||
presets = data.get('presets', {})
|
|
||||||
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
|
|
||||||
print(pid)
|
|
||||||
except Exception:
|
|
||||||
sys.exit(1)
|
|
||||||
" 2>/dev/null); then
|
|
||||||
if [ -n "$sorted_presets" ]; then
|
|
||||||
# python3 succeeded and returned preset IDs — search in priority order
|
|
||||||
while IFS= read -r preset_id; do
|
|
||||||
local candidate="$presets_dir/$preset_id/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done <<< "$sorted_presets"
|
|
||||||
fi
|
|
||||||
# python3 succeeded but registry has no presets — nothing to search
|
|
||||||
else
|
|
||||||
# python3 failed (missing, or registry parse error) — fall back to unordered directory scan
|
|
||||||
for preset in "$presets_dir"/*/; do
|
|
||||||
[ -d "$preset" ] || continue
|
|
||||||
local candidate="$preset/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Fallback: alphabetical directory order (no python3 available)
|
|
||||||
for preset in "$presets_dir"/*/; do
|
|
||||||
[ -d "$preset" ] || continue
|
|
||||||
local candidate="$preset/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Priority 3: Extension-provided templates
|
|
||||||
local ext_dir="$repo_root/.specify/extensions"
|
|
||||||
if [ -d "$ext_dir" ]; then
|
|
||||||
for ext in "$ext_dir"/*/; do
|
|
||||||
[ -d "$ext" ] || continue
|
|
||||||
# Skip hidden directories (e.g. .backup, .cache)
|
|
||||||
case "$(basename "$ext")" in .*) continue;; esac
|
|
||||||
local candidate="$ext/templates/${template_name}.md"
|
|
||||||
[ -f "$candidate" ] && echo "$candidate" && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Priority 4: Core templates
|
|
||||||
local core="$base/${template_name}.md"
|
|
||||||
[ -f "$core" ] && echo "$core" && return 0
|
|
||||||
|
|
||||||
# Template not found in any location.
|
|
||||||
# Return 1 so callers can distinguish "not found" from "found".
|
|
||||||
# Callers running under set -e should use: TEMPLATE=$(resolve_template ...) || true
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,15 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
JSON_MODE=false
|
JSON_MODE=false
|
||||||
DRY_RUN=false
|
|
||||||
ALLOW_EXISTING=false
|
|
||||||
SHORT_NAME=""
|
SHORT_NAME=""
|
||||||
BRANCH_NUMBER=""
|
BRANCH_NUMBER=""
|
||||||
USE_TIMESTAMP=false
|
|
||||||
ARGS=()
|
ARGS=()
|
||||||
i=1
|
i=1
|
||||||
while [ $i -le $# ]; do
|
while [ $i -le $# ]; do
|
||||||
arg="${!i}"
|
arg="${!i}"
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--json)
|
--json)
|
||||||
JSON_MODE=true
|
JSON_MODE=true
|
||||||
;;
|
|
||||||
--dry-run)
|
|
||||||
DRY_RUN=true
|
|
||||||
;;
|
|
||||||
--allow-existing-branch)
|
|
||||||
ALLOW_EXISTING=true
|
|
||||||
;;
|
;;
|
||||||
--short-name)
|
--short-name)
|
||||||
if [ $((i + 1)) -gt $# ]; then
|
if [ $((i + 1)) -gt $# ]; then
|
||||||
@@ -49,29 +40,22 @@ while [ $i -le $# ]; do
|
|||||||
fi
|
fi
|
||||||
BRANCH_NUMBER="$next_arg"
|
BRANCH_NUMBER="$next_arg"
|
||||||
;;
|
;;
|
||||||
--timestamp)
|
--help|-h)
|
||||||
USE_TIMESTAMP=true
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
||||||
;;
|
|
||||||
--help|-h)
|
|
||||||
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
echo " --json Output in JSON format"
|
echo " --json Output in JSON format"
|
||||||
echo " --dry-run Compute branch name and paths without creating branches, directories, or files"
|
|
||||||
echo " --allow-existing-branch Switch to branch if it already exists instead of failing"
|
|
||||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||||
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
|
||||||
echo " --help, -h Show this help message"
|
echo " --help, -h Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "Examples:"
|
||||||
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
||||||
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
||||||
echo " $0 --timestamp --short-name 'user-auth' 'Add user authentication'"
|
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
ARGS+=("$arg")
|
ARGS+=("$arg")
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
@@ -79,7 +63,7 @@ done
|
|||||||
|
|
||||||
FEATURE_DESCRIPTION="${ARGS[*]}"
|
FEATURE_DESCRIPTION="${ARGS[*]}"
|
||||||
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||||
echo "Usage: $0 [--json] [--dry-run] [--allow-existing-branch] [--short-name <name>] [--number N] [--timestamp] <feature_description>" >&2
|
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -90,6 +74,19 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Function to find the repository root by searching for existing project markers
|
||||||
|
find_repo_root() {
|
||||||
|
local dir="$1"
|
||||||
|
while [ "$dir" != "/" ]; do
|
||||||
|
if [ -d "$dir/.git" ] || [ -d "$dir/.specify" ]; then
|
||||||
|
echo "$dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
dir="$(dirname "$dir")"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# Function to get highest number from specs directory
|
# Function to get highest number from specs directory
|
||||||
get_highest_from_specs() {
|
get_highest_from_specs() {
|
||||||
local specs_dir="$1"
|
local specs_dir="$1"
|
||||||
@@ -99,13 +96,10 @@ get_highest_from_specs() {
|
|||||||
for dir in "$specs_dir"/*; do
|
for dir in "$specs_dir"/*; do
|
||||||
[ -d "$dir" ] || continue
|
[ -d "$dir" ] || continue
|
||||||
dirname=$(basename "$dir")
|
dirname=$(basename "$dir")
|
||||||
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
|
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
||||||
if echo "$dirname" | grep -Eq '^[0-9]{3,}-' && ! echo "$dirname" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
number=$((10#$number))
|
||||||
number=$(echo "$dirname" | grep -Eo '^[0-9]+')
|
if [ "$number" -gt "$highest" ]; then
|
||||||
number=$((10#$number))
|
highest=$number
|
||||||
if [ "$number" -gt "$highest" ]; then
|
|
||||||
highest=$number
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
@@ -115,59 +109,39 @@ get_highest_from_specs() {
|
|||||||
|
|
||||||
# Function to get highest number from git branches
|
# Function to get highest number from git branches
|
||||||
get_highest_from_branches() {
|
get_highest_from_branches() {
|
||||||
git branch -a 2>/dev/null | sed 's/^[* ]*//; s|^remotes/[^/]*/||' | _extract_highest_number
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract the highest sequential feature number from a list of ref names (one per line).
|
|
||||||
# Shared by get_highest_from_branches and get_highest_from_remote_refs.
|
|
||||||
_extract_highest_number() {
|
|
||||||
local highest=0
|
local highest=0
|
||||||
while IFS= read -r name; do
|
|
||||||
[ -z "$name" ] && continue
|
# Get all branches (local and remote)
|
||||||
if echo "$name" | grep -Eq '^[0-9]{3,}-' && ! echo "$name" | grep -Eq '^[0-9]{8}-[0-9]{6}-'; then
|
branches=$(git branch -a 2>/dev/null || echo "")
|
||||||
number=$(echo "$name" | grep -Eo '^[0-9]+' || echo "0")
|
|
||||||
number=$((10#$number))
|
if [ -n "$branches" ]; then
|
||||||
if [ "$number" -gt "$highest" ]; then
|
while IFS= read -r branch; do
|
||||||
highest=$number
|
# Clean branch name: remove leading markers and remote prefixes
|
||||||
|
clean_branch=$(echo "$branch" | sed 's/^[* ]*//; s|^remotes/[^/]*/||')
|
||||||
|
|
||||||
|
# Extract feature number if branch matches pattern ###-*
|
||||||
|
if echo "$clean_branch" | grep -q '^[0-9]\{3\}-'; then
|
||||||
|
number=$(echo "$clean_branch" | grep -o '^[0-9]\{3\}' || echo "0")
|
||||||
|
number=$((10#$number))
|
||||||
|
if [ "$number" -gt "$highest" ]; then
|
||||||
|
highest=$number
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
done <<< "$branches"
|
||||||
done
|
fi
|
||||||
|
|
||||||
echo "$highest"
|
echo "$highest"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Function to get highest number from remote branches without fetching (side-effect-free)
|
# Function to check existing branches (local and remote) and return next available number
|
||||||
get_highest_from_remote_refs() {
|
|
||||||
local highest=0
|
|
||||||
|
|
||||||
for remote in $(git remote 2>/dev/null); do
|
|
||||||
local remote_highest
|
|
||||||
remote_highest=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads "$remote" 2>/dev/null | sed 's|.*refs/heads/||' | _extract_highest_number)
|
|
||||||
if [ "$remote_highest" -gt "$highest" ]; then
|
|
||||||
highest=$remote_highest
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "$highest"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to check existing branches (local and remote) and return next available number.
|
|
||||||
# When skip_fetch is true, queries remotes via ls-remote (read-only) instead of fetching.
|
|
||||||
check_existing_branches() {
|
check_existing_branches() {
|
||||||
local specs_dir="$1"
|
local specs_dir="$1"
|
||||||
local skip_fetch="${2:-false}"
|
|
||||||
|
|
||||||
if [ "$skip_fetch" = true ]; then
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
# Side-effect-free: query remotes via ls-remote
|
git fetch --all --prune 2>/dev/null || true
|
||||||
local highest_remote=$(get_highest_from_remote_refs)
|
|
||||||
local highest_branch=$(get_highest_from_branches)
|
# Get highest number from ALL branches (not just matching short name)
|
||||||
if [ "$highest_remote" -gt "$highest_branch" ]; then
|
local highest_branch=$(get_highest_from_branches)
|
||||||
highest_branch=$highest_remote
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
|
||||||
git fetch --all --prune >/dev/null 2>&1 || true
|
|
||||||
local highest_branch=$(get_highest_from_branches)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get highest number from ALL specs (not just matching short name)
|
# Get highest number from ALL specs (not just matching short name)
|
||||||
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
local highest_spec=$(get_highest_from_specs "$specs_dir")
|
||||||
@@ -188,25 +162,27 @@ clean_branch_name() {
|
|||||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve repository root using common.sh functions which prioritize .specify over git
|
# Resolve repository root. Prefer git information when available, but fall back
|
||||||
|
# to searching for repository markers so the workflow still functions in repositories that
|
||||||
|
# were initialised with --no-git.
|
||||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
source "$SCRIPT_DIR/common.sh"
|
|
||||||
|
|
||||||
REPO_ROOT=$(get_repo_root)
|
if git rev-parse --show-toplevel >/dev/null 2>&1; then
|
||||||
|
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||||
# Check if git is available at this repo root (not a parent)
|
|
||||||
if has_git; then
|
|
||||||
HAS_GIT=true
|
HAS_GIT=true
|
||||||
else
|
else
|
||||||
|
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
||||||
|
if [ -z "$REPO_ROOT" ]; then
|
||||||
|
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
HAS_GIT=false
|
HAS_GIT=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd "$REPO_ROOT"
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
SPECS_DIR="$REPO_ROOT/specs"
|
SPECS_DIR="$REPO_ROOT/specs"
|
||||||
if [ "$DRY_RUN" != true ]; then
|
mkdir -p "$SPECS_DIR"
|
||||||
mkdir -p "$SPECS_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
generate_branch_name() {
|
generate_branch_name() {
|
||||||
@@ -265,49 +241,29 @@ else
|
|||||||
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Warn if --number and --timestamp are both specified
|
# Determine branch number
|
||||||
if [ "$USE_TIMESTAMP" = true ] && [ -n "$BRANCH_NUMBER" ]; then
|
if [ -z "$BRANCH_NUMBER" ]; then
|
||||||
>&2 echo "[specify] Warning: --number is ignored when --timestamp is used"
|
if [ "$HAS_GIT" = true ]; then
|
||||||
BRANCH_NUMBER=""
|
# Check existing branches on remotes
|
||||||
fi
|
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
||||||
|
else
|
||||||
# Determine branch prefix
|
# Fall back to local directory check
|
||||||
if [ "$USE_TIMESTAMP" = true ]; then
|
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||||
FEATURE_NUM=$(date +%Y%m%d-%H%M%S)
|
BRANCH_NUMBER=$((HIGHEST + 1))
|
||||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
||||||
else
|
|
||||||
# Determine branch number
|
|
||||||
if [ -z "$BRANCH_NUMBER" ]; then
|
|
||||||
if [ "$DRY_RUN" = true ] && [ "$HAS_GIT" = true ]; then
|
|
||||||
# Dry-run: query remotes via ls-remote (side-effect-free, no fetch)
|
|
||||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR" true)
|
|
||||||
elif [ "$DRY_RUN" = true ]; then
|
|
||||||
# Dry-run without git: local spec dirs only
|
|
||||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
||||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
|
||||||
elif [ "$HAS_GIT" = true ]; then
|
|
||||||
# Check existing branches on remotes
|
|
||||||
BRANCH_NUMBER=$(check_existing_branches "$SPECS_DIR")
|
|
||||||
else
|
|
||||||
# Fall back to local directory check
|
|
||||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
||||||
BRANCH_NUMBER=$((HIGHEST + 1))
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
|
||||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
|
||||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
||||||
|
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||||
|
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||||
|
|
||||||
# GitHub enforces a 244-byte limit on branch names
|
# GitHub enforces a 244-byte limit on branch names
|
||||||
# Validate and truncate if necessary
|
# Validate and truncate if necessary
|
||||||
MAX_BRANCH_LENGTH=244
|
MAX_BRANCH_LENGTH=244
|
||||||
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
||||||
# Calculate how much we need to trim from suffix
|
# Calculate how much we need to trim from suffix
|
||||||
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
|
# Account for: feature number (3) + hyphen (1) = 4 chars
|
||||||
PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))
|
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
|
||||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
|
|
||||||
|
|
||||||
# Truncate suffix at word boundary if possible
|
# Truncate suffix at word boundary if possible
|
||||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||||
@@ -322,79 +278,36 @@ if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
|||||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
if [ "$HAS_GIT" = true ]; then
|
||||||
SPEC_FILE="$FEATURE_DIR/spec.md"
|
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
||||||
|
# Check if branch already exists
|
||||||
if [ "$DRY_RUN" != true ]; then
|
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
||||||
if [ "$HAS_GIT" = true ]; then
|
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
||||||
if ! git checkout -b "$BRANCH_NAME" 2>/dev/null; then
|
exit 1
|
||||||
# Check if branch already exists
|
|
||||||
if git branch --list "$BRANCH_NAME" | grep -q .; then
|
|
||||||
if [ "$ALLOW_EXISTING" = true ]; then
|
|
||||||
# Switch to the existing branch instead of failing
|
|
||||||
if ! git checkout "$BRANCH_NAME" 2>/dev/null; then
|
|
||||||
>&2 echo "Error: Failed to switch to existing branch '$BRANCH_NAME'. Please resolve any local changes or conflicts and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
elif [ "$USE_TIMESTAMP" = true ]; then
|
|
||||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Rerun to get a new timestamp or use a different --short-name."
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
>&2 echo "Error: Branch '$BRANCH_NAME' already exists. Please use a different feature name or specify a different number with --number."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$FEATURE_DIR"
|
|
||||||
|
|
||||||
if [ ! -f "$SPEC_FILE" ]; then
|
|
||||||
TEMPLATE=$(resolve_template "spec-template" "$REPO_ROOT") || true
|
|
||||||
if [ -n "$TEMPLATE" ] && [ -f "$TEMPLATE" ]; then
|
|
||||||
cp "$TEMPLATE" "$SPEC_FILE"
|
|
||||||
else
|
else
|
||||||
echo "Warning: Spec template not found; created empty spec file" >&2
|
>&2 echo "Error: Failed to create git branch '$BRANCH_NAME'. Please check your git configuration and try again."
|
||||||
touch "$SPEC_FILE"
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
# Inform the user how to persist the feature variable in their own shell
|
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
|
||||||
printf '# To persist: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME" >&2
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
||||||
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
|
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
|
||||||
|
SPEC_FILE="$FEATURE_DIR/spec.md"
|
||||||
|
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
||||||
|
|
||||||
|
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||||
|
export SPECIFY_FEATURE="$BRANCH_NAME"
|
||||||
|
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
if command -v jq >/dev/null 2>&1; then
|
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
||||||
if [ "$DRY_RUN" = true ]; then
|
|
||||||
jq -cn \
|
|
||||||
--arg branch_name "$BRANCH_NAME" \
|
|
||||||
--arg spec_file "$SPEC_FILE" \
|
|
||||||
--arg feature_num "$FEATURE_NUM" \
|
|
||||||
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num,DRY_RUN:true}'
|
|
||||||
else
|
|
||||||
jq -cn \
|
|
||||||
--arg branch_name "$BRANCH_NAME" \
|
|
||||||
--arg spec_file "$SPEC_FILE" \
|
|
||||||
--arg feature_num "$FEATURE_NUM" \
|
|
||||||
'{BRANCH_NAME:$branch_name,SPEC_FILE:$spec_file,FEATURE_NUM:$feature_num}'
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ "$DRY_RUN" = true ]; then
|
|
||||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s","DRY_RUN":true}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
|
||||||
else
|
|
||||||
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$(json_escape "$BRANCH_NAME")" "$(json_escape "$SPEC_FILE")" "$(json_escape "$FEATURE_NUM")"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||||
echo "SPEC_FILE: $SPEC_FILE"
|
echo "SPEC_FILE: $SPEC_FILE"
|
||||||
echo "FEATURE_NUM: $FEATURE_NUM"
|
echo "FEATURE_NUM: $FEATURE_NUM"
|
||||||
if [ "$DRY_RUN" != true ]; then
|
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
||||||
printf '# To persist in your shell: export SPECIFY_FEATURE=%q\n' "$BRANCH_NAME"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/common.sh"
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
# Get all paths and variables from common functions
|
# Get all paths and variables from common functions
|
||||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
eval $(get_feature_paths)
|
||||||
eval "$_paths_output"
|
|
||||||
unset _paths_output
|
|
||||||
|
|
||||||
# Check if we're on a proper feature branch (only for git repos)
|
# Check if we're on a proper feature branch (only for git repos)
|
||||||
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
||||||
@@ -39,30 +37,20 @@ check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
|
|||||||
mkdir -p "$FEATURE_DIR"
|
mkdir -p "$FEATURE_DIR"
|
||||||
|
|
||||||
# Copy plan template if it exists
|
# Copy plan template if it exists
|
||||||
TEMPLATE=$(resolve_template "plan-template" "$REPO_ROOT") || true
|
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
|
||||||
if [[ -n "$TEMPLATE" ]] && [[ -f "$TEMPLATE" ]]; then
|
if [[ -f "$TEMPLATE" ]]; then
|
||||||
cp "$TEMPLATE" "$IMPL_PLAN"
|
cp "$TEMPLATE" "$IMPL_PLAN"
|
||||||
echo "Copied plan template to $IMPL_PLAN"
|
echo "Copied plan template to $IMPL_PLAN"
|
||||||
else
|
else
|
||||||
echo "Warning: Plan template not found"
|
echo "Warning: Plan template not found at $TEMPLATE"
|
||||||
# Create a basic plan file if template doesn't exist
|
# Create a basic plan file if template doesn't exist
|
||||||
touch "$IMPL_PLAN"
|
touch "$IMPL_PLAN"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Output results
|
# Output results
|
||||||
if $JSON_MODE; then
|
if $JSON_MODE; then
|
||||||
if has_jq; then
|
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
||||||
jq -cn \
|
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
|
||||||
--arg feature_spec "$FEATURE_SPEC" \
|
|
||||||
--arg impl_plan "$IMPL_PLAN" \
|
|
||||||
--arg specs_dir "$FEATURE_DIR" \
|
|
||||||
--arg branch "$CURRENT_BRANCH" \
|
|
||||||
--arg has_git "$HAS_GIT" \
|
|
||||||
'{FEATURE_SPEC:$feature_spec,IMPL_PLAN:$impl_plan,SPECS_DIR:$specs_dir,BRANCH:$branch,HAS_GIT:$has_git}'
|
|
||||||
else
|
|
||||||
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
|
|
||||||
"$(json_escape "$FEATURE_SPEC")" "$(json_escape "$IMPL_PLAN")" "$(json_escape "$FEATURE_DIR")" "$(json_escape "$CURRENT_BRANCH")" "$(json_escape "$HAS_GIT")"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
echo "FEATURE_SPEC: $FEATURE_SPEC"
|
||||||
echo "IMPL_PLAN: $IMPL_PLAN"
|
echo "IMPL_PLAN: $IMPL_PLAN"
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
#
|
#
|
||||||
# 5. Multi-Agent Support
|
# 5. Multi-Agent Support
|
||||||
# - Handles agent-specific file paths and naming conventions
|
# - Handles agent-specific file paths and naming conventions
|
||||||
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Junie, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Tabnine CLI, Kiro CLI, Mistral Vibe, Kimi Code, Pi Coding Agent, iFlow CLI, Antigravity or Generic
|
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Kiro CLI, or Antigravity
|
||||||
# - Can update single agents or all existing agent files
|
# - Can update single agents or all existing agent files
|
||||||
# - Creates default Claude file if no agent files exist
|
# - Creates default Claude file if no agent files exist
|
||||||
#
|
#
|
||||||
# Usage: ./update-agent-context.sh [agent_type]
|
# Usage: ./update-agent-context.sh [agent_type]
|
||||||
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic
|
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli
|
||||||
# Leave empty to update all existing agent files
|
# Leave empty to update all existing agent files
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@@ -53,9 +53,7 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|||||||
source "$SCRIPT_DIR/common.sh"
|
source "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
# Get all paths and variables from common functions
|
# Get all paths and variables from common functions
|
||||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
eval $(get_feature_paths)
|
||||||
eval "$_paths_output"
|
|
||||||
unset _paths_output
|
|
||||||
|
|
||||||
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
|
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
|
||||||
AGENT_TYPE="${1:-}"
|
AGENT_TYPE="${1:-}"
|
||||||
@@ -63,29 +61,21 @@ AGENT_TYPE="${1:-}"
|
|||||||
# Agent-specific file paths
|
# Agent-specific file paths
|
||||||
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
|
||||||
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
|
||||||
COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
|
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
|
||||||
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
|
||||||
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
QWEN_FILE="$REPO_ROOT/QWEN.md"
|
||||||
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
|
||||||
JUNIE_FILE="$REPO_ROOT/.junie/AGENTS.md"
|
|
||||||
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
|
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
|
||||||
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
|
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
|
||||||
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
|
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
|
||||||
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
|
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
|
||||||
QODER_FILE="$REPO_ROOT/QODER.md"
|
QODER_FILE="$REPO_ROOT/QODER.md"
|
||||||
# Amp, Kiro CLI, IBM Bob, and Pi all share AGENTS.md — use AGENTS_FILE to avoid
|
AMP_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
# updating the same file multiple times.
|
|
||||||
AMP_FILE="$AGENTS_FILE"
|
|
||||||
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
SHAI_FILE="$REPO_ROOT/SHAI.md"
|
||||||
TABNINE_FILE="$REPO_ROOT/TABNINE.md"
|
KIRO_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
KIRO_FILE="$AGENTS_FILE"
|
|
||||||
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
|
||||||
BOB_FILE="$AGENTS_FILE"
|
BOB_FILE="$REPO_ROOT/AGENTS.md"
|
||||||
VIBE_FILE="$REPO_ROOT/.vibe/agents/specify-agents.md"
|
|
||||||
KIMI_FILE="$REPO_ROOT/KIMI.md"
|
|
||||||
TRAE_FILE="$REPO_ROOT/.trae/rules/AGENTS.md"
|
|
||||||
IFLOW_FILE="$REPO_ROOT/IFLOW.md"
|
|
||||||
|
|
||||||
# Template file
|
# Template file
|
||||||
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
|
||||||
@@ -119,8 +109,6 @@ log_warning() {
|
|||||||
# Cleanup function for temporary files
|
# Cleanup function for temporary files
|
||||||
cleanup() {
|
cleanup() {
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
# Disarm traps to prevent re-entrant loop
|
|
||||||
trap - EXIT INT TERM
|
|
||||||
rm -f /tmp/agent_update_*_$$
|
rm -f /tmp/agent_update_*_$$
|
||||||
rm -f /tmp/manual_additions_$$
|
rm -f /tmp/manual_additions_$$
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
@@ -485,7 +473,7 @@ update_existing_agent_file() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Update timestamp
|
# Update timestamp
|
||||||
if [[ "$line" =~ (\*\*)?Last\ updated(\*\*)?:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
|
if [[ "$line" =~ \*\*Last\ updated\*\*:.*[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] ]]; then
|
||||||
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
|
echo "$line" | sed "s/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/$current_date/" >> "$temp_file"
|
||||||
else
|
else
|
||||||
echo "$line" >> "$temp_file"
|
echo "$line" >> "$temp_file"
|
||||||
@@ -616,155 +604,158 @@ update_specific_agent() {
|
|||||||
|
|
||||||
case "$agent_type" in
|
case "$agent_type" in
|
||||||
claude)
|
claude)
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||||
;;
|
;;
|
||||||
gemini)
|
gemini)
|
||||||
update_agent_file "$GEMINI_FILE" "Gemini CLI" || return 1
|
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||||
;;
|
;;
|
||||||
copilot)
|
copilot)
|
||||||
update_agent_file "$COPILOT_FILE" "GitHub Copilot" || return 1
|
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||||
;;
|
;;
|
||||||
cursor-agent)
|
cursor-agent)
|
||||||
update_agent_file "$CURSOR_FILE" "Cursor IDE" || return 1
|
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
||||||
;;
|
;;
|
||||||
qwen)
|
qwen)
|
||||||
update_agent_file "$QWEN_FILE" "Qwen Code" || return 1
|
update_agent_file "$QWEN_FILE" "Qwen Code"
|
||||||
;;
|
;;
|
||||||
opencode)
|
opencode)
|
||||||
update_agent_file "$AGENTS_FILE" "opencode" || return 1
|
update_agent_file "$AGENTS_FILE" "opencode"
|
||||||
;;
|
;;
|
||||||
codex)
|
codex)
|
||||||
update_agent_file "$AGENTS_FILE" "Codex CLI" || return 1
|
update_agent_file "$AGENTS_FILE" "Codex CLI"
|
||||||
;;
|
;;
|
||||||
windsurf)
|
windsurf)
|
||||||
update_agent_file "$WINDSURF_FILE" "Windsurf" || return 1
|
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
||||||
;;
|
|
||||||
junie)
|
|
||||||
update_agent_file "$JUNIE_FILE" "Junie" || return 1
|
|
||||||
;;
|
;;
|
||||||
kilocode)
|
kilocode)
|
||||||
update_agent_file "$KILOCODE_FILE" "Kilo Code" || return 1
|
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
||||||
;;
|
;;
|
||||||
auggie)
|
auggie)
|
||||||
update_agent_file "$AUGGIE_FILE" "Auggie CLI" || return 1
|
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
||||||
;;
|
;;
|
||||||
roo)
|
roo)
|
||||||
update_agent_file "$ROO_FILE" "Roo Code" || return 1
|
update_agent_file "$ROO_FILE" "Roo Code"
|
||||||
;;
|
;;
|
||||||
codebuddy)
|
codebuddy)
|
||||||
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI" || return 1
|
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||||
;;
|
;;
|
||||||
qodercli)
|
qodercli)
|
||||||
update_agent_file "$QODER_FILE" "Qoder CLI" || return 1
|
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||||
;;
|
;;
|
||||||
amp)
|
amp)
|
||||||
update_agent_file "$AMP_FILE" "Amp" || return 1
|
update_agent_file "$AMP_FILE" "Amp"
|
||||||
;;
|
;;
|
||||||
shai)
|
shai)
|
||||||
update_agent_file "$SHAI_FILE" "SHAI" || return 1
|
update_agent_file "$SHAI_FILE" "SHAI"
|
||||||
;;
|
|
||||||
tabnine)
|
|
||||||
update_agent_file "$TABNINE_FILE" "Tabnine CLI" || return 1
|
|
||||||
;;
|
;;
|
||||||
kiro-cli)
|
kiro-cli)
|
||||||
update_agent_file "$KIRO_FILE" "Kiro CLI" || return 1
|
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
||||||
;;
|
;;
|
||||||
agy)
|
agy)
|
||||||
update_agent_file "$AGY_FILE" "Antigravity" || return 1
|
update_agent_file "$AGY_FILE" "Antigravity"
|
||||||
;;
|
;;
|
||||||
bob)
|
bob)
|
||||||
update_agent_file "$BOB_FILE" "IBM Bob" || return 1
|
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||||
;;
|
|
||||||
vibe)
|
|
||||||
update_agent_file "$VIBE_FILE" "Mistral Vibe" || return 1
|
|
||||||
;;
|
|
||||||
kimi)
|
|
||||||
update_agent_file "$KIMI_FILE" "Kimi Code" || return 1
|
|
||||||
;;
|
|
||||||
trae)
|
|
||||||
update_agent_file "$TRAE_FILE" "Trae" || return 1
|
|
||||||
;;
|
|
||||||
pi)
|
|
||||||
update_agent_file "$AGENTS_FILE" "Pi Coding Agent" || return 1
|
|
||||||
;;
|
|
||||||
iflow)
|
|
||||||
update_agent_file "$IFLOW_FILE" "iFlow CLI" || return 1
|
|
||||||
;;
|
;;
|
||||||
generic)
|
generic)
|
||||||
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
log_info "Generic agent: no predefined context file. Use the agent-specific update script for your agent."
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
log_error "Unknown agent type '$agent_type'"
|
log_error "Unknown agent type '$agent_type'"
|
||||||
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic"
|
log_error "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper: skip non-existent files and files already updated (dedup by
|
|
||||||
# realpath so that variables pointing to the same file — e.g. AMP_FILE,
|
|
||||||
# KIRO_FILE, BOB_FILE all resolving to AGENTS_FILE — are only written once).
|
|
||||||
# Uses a linear array instead of associative array for bash 3.2 compatibility.
|
|
||||||
# Note: defined at top level because bash 3.2 does not support true
|
|
||||||
# nested/local functions. _updated_paths, _found_agent, and _all_ok are
|
|
||||||
# initialised exclusively inside update_all_existing_agents so that
|
|
||||||
# sourcing this script has no side effects on the caller's environment.
|
|
||||||
|
|
||||||
_update_if_new() {
|
|
||||||
local file="$1" name="$2"
|
|
||||||
[[ -f "$file" ]] || return 0
|
|
||||||
local real_path
|
|
||||||
real_path=$(realpath "$file" 2>/dev/null || echo "$file")
|
|
||||||
local p
|
|
||||||
if [[ ${#_updated_paths[@]} -gt 0 ]]; then
|
|
||||||
for p in "${_updated_paths[@]}"; do
|
|
||||||
[[ "$p" == "$real_path" ]] && return 0
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
# Record the file as seen before attempting the update so that:
|
|
||||||
# (a) aliases pointing to the same path are not retried on failure
|
|
||||||
# (b) _found_agent reflects file existence, not update success
|
|
||||||
_updated_paths+=("$real_path")
|
|
||||||
_found_agent=true
|
|
||||||
update_agent_file "$file" "$name"
|
|
||||||
}
|
|
||||||
|
|
||||||
update_all_existing_agents() {
|
update_all_existing_agents() {
|
||||||
_found_agent=false
|
local found_agent=false
|
||||||
_updated_paths=()
|
|
||||||
local _all_ok=true
|
# Check each possible agent file and update if it exists
|
||||||
|
if [[ -f "$CLAUDE_FILE" ]]; then
|
||||||
_update_if_new "$CLAUDE_FILE" "Claude Code" || _all_ok=false
|
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||||
_update_if_new "$GEMINI_FILE" "Gemini CLI" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$COPILOT_FILE" "GitHub Copilot" || _all_ok=false
|
fi
|
||||||
_update_if_new "$CURSOR_FILE" "Cursor IDE" || _all_ok=false
|
|
||||||
_update_if_new "$QWEN_FILE" "Qwen Code" || _all_ok=false
|
if [[ -f "$GEMINI_FILE" ]]; then
|
||||||
_update_if_new "$AGENTS_FILE" "Codex/opencode" || _all_ok=false
|
update_agent_file "$GEMINI_FILE" "Gemini CLI"
|
||||||
_update_if_new "$AMP_FILE" "Amp" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$KIRO_FILE" "Kiro CLI" || _all_ok=false
|
fi
|
||||||
_update_if_new "$BOB_FILE" "IBM Bob" || _all_ok=false
|
|
||||||
_update_if_new "$WINDSURF_FILE" "Windsurf" || _all_ok=false
|
if [[ -f "$COPILOT_FILE" ]]; then
|
||||||
_update_if_new "$JUNIE_FILE" "Junie" || _all_ok=false
|
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
|
||||||
_update_if_new "$KILOCODE_FILE" "Kilo Code" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$AUGGIE_FILE" "Auggie CLI" || _all_ok=false
|
fi
|
||||||
_update_if_new "$ROO_FILE" "Roo Code" || _all_ok=false
|
|
||||||
_update_if_new "$CODEBUDDY_FILE" "CodeBuddy CLI" || _all_ok=false
|
if [[ -f "$CURSOR_FILE" ]]; then
|
||||||
_update_if_new "$SHAI_FILE" "SHAI" || _all_ok=false
|
update_agent_file "$CURSOR_FILE" "Cursor IDE"
|
||||||
_update_if_new "$TABNINE_FILE" "Tabnine CLI" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$QODER_FILE" "Qoder CLI" || _all_ok=false
|
fi
|
||||||
_update_if_new "$AGY_FILE" "Antigravity" || _all_ok=false
|
|
||||||
_update_if_new "$VIBE_FILE" "Mistral Vibe" || _all_ok=false
|
if [[ -f "$QWEN_FILE" ]]; then
|
||||||
_update_if_new "$KIMI_FILE" "Kimi Code" || _all_ok=false
|
update_agent_file "$QWEN_FILE" "Qwen Code"
|
||||||
_update_if_new "$TRAE_FILE" "Trae" || _all_ok=false
|
found_agent=true
|
||||||
_update_if_new "$IFLOW_FILE" "iFlow CLI" || _all_ok=false
|
fi
|
||||||
|
|
||||||
# If no agent files exist, create a default Claude file
|
if [[ -f "$AGENTS_FILE" ]]; then
|
||||||
if [[ "$_found_agent" == false ]]; then
|
update_agent_file "$AGENTS_FILE" "Codex/opencode"
|
||||||
log_info "No existing agent files found, creating default Claude file..."
|
found_agent=true
|
||||||
update_agent_file "$CLAUDE_FILE" "Claude Code" || return 1
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WINDSURF_FILE" ]]; then
|
||||||
|
update_agent_file "$WINDSURF_FILE" "Windsurf"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$KILOCODE_FILE" ]]; then
|
||||||
|
update_agent_file "$KILOCODE_FILE" "Kilo Code"
|
||||||
|
found_agent=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ "$_all_ok" == true ]]
|
if [[ -f "$AUGGIE_FILE" ]]; then
|
||||||
|
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$ROO_FILE" ]]; then
|
||||||
|
update_agent_file "$ROO_FILE" "Roo Code"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$CODEBUDDY_FILE" ]]; then
|
||||||
|
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$SHAI_FILE" ]]; then
|
||||||
|
update_agent_file "$SHAI_FILE" "SHAI"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$QODER_FILE" ]]; then
|
||||||
|
update_agent_file "$QODER_FILE" "Qoder CLI"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$KIRO_FILE" ]]; then
|
||||||
|
update_agent_file "$KIRO_FILE" "Kiro CLI"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$AGY_FILE" ]]; then
|
||||||
|
update_agent_file "$AGY_FILE" "Antigravity"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
if [[ -f "$BOB_FILE" ]]; then
|
||||||
|
update_agent_file "$BOB_FILE" "IBM Bob"
|
||||||
|
found_agent=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If no agent files exist, create a default Claude file
|
||||||
|
if [[ "$found_agent" == false ]]; then
|
||||||
|
log_info "No existing agent files found, creating default Claude file..."
|
||||||
|
update_agent_file "$CLAUDE_FILE" "Claude Code"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
print_summary() {
|
print_summary() {
|
||||||
echo
|
echo
|
||||||
@@ -783,7 +774,8 @@ print_summary() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]"
|
|
||||||
|
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli]"
|
||||||
}
|
}
|
||||||
|
|
||||||
#==============================================================================
|
#==============================================================================
|
||||||
|
|||||||
@@ -1,39 +1,7 @@
|
|||||||
#!/usr/bin/env pwsh
|
#!/usr/bin/env pwsh
|
||||||
# Common PowerShell functions analogous to common.sh
|
# Common PowerShell functions analogous to common.sh
|
||||||
|
|
||||||
# Find repository root by searching upward for .specify directory
|
|
||||||
# This is the primary marker for spec-kit projects
|
|
||||||
function Find-SpecifyRoot {
|
|
||||||
param([string]$StartDir = (Get-Location).Path)
|
|
||||||
|
|
||||||
# Normalize to absolute path to prevent issues with relative paths
|
|
||||||
# Use -LiteralPath to handle paths with wildcard characters ([, ], *, ?)
|
|
||||||
$resolved = Resolve-Path -LiteralPath $StartDir -ErrorAction SilentlyContinue
|
|
||||||
$current = if ($resolved) { $resolved.Path } else { $null }
|
|
||||||
if (-not $current) { return $null }
|
|
||||||
|
|
||||||
while ($true) {
|
|
||||||
if (Test-Path -LiteralPath (Join-Path $current ".specify") -PathType Container) {
|
|
||||||
return $current
|
|
||||||
}
|
|
||||||
$parent = Split-Path $current -Parent
|
|
||||||
if ([string]::IsNullOrEmpty($parent) -or $parent -eq $current) {
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
$current = $parent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get repository root, prioritizing .specify directory over git
|
|
||||||
# This prevents using a parent git repo when spec-kit is initialized in a subdirectory
|
|
||||||
function Get-RepoRoot {
|
function Get-RepoRoot {
|
||||||
# First, look for .specify directory (spec-kit's own marker)
|
|
||||||
$specifyRoot = Find-SpecifyRoot
|
|
||||||
if ($specifyRoot) {
|
|
||||||
return $specifyRoot
|
|
||||||
}
|
|
||||||
|
|
||||||
# Fallback to git if no .specify found
|
|
||||||
try {
|
try {
|
||||||
$result = git rev-parse --show-toplevel 2>$null
|
$result = git rev-parse --show-toplevel 2>$null
|
||||||
if ($LASTEXITCODE -eq 0) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
@@ -42,10 +10,9 @@ function Get-RepoRoot {
|
|||||||
} catch {
|
} catch {
|
||||||
# Git command failed
|
# Git command failed
|
||||||
}
|
}
|
||||||
|
|
||||||
# Final fallback to script location for non-git repos
|
# Fall back to script location for non-git repos
|
||||||
# Use -LiteralPath to handle paths with wildcard characters
|
return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path
|
||||||
return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "../../..")).Path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-CurrentBranch {
|
function Get-CurrentBranch {
|
||||||
@@ -53,48 +20,35 @@ function Get-CurrentBranch {
|
|||||||
if ($env:SPECIFY_FEATURE) {
|
if ($env:SPECIFY_FEATURE) {
|
||||||
return $env:SPECIFY_FEATURE
|
return $env:SPECIFY_FEATURE
|
||||||
}
|
}
|
||||||
|
|
||||||
# Then check git if available at the spec-kit root (not parent)
|
# Then check git if available
|
||||||
$repoRoot = Get-RepoRoot
|
try {
|
||||||
if (Test-HasGit) {
|
$result = git rev-parse --abbrev-ref HEAD 2>$null
|
||||||
try {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$result = git -C $repoRoot rev-parse --abbrev-ref HEAD 2>$null
|
return $result
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
return $result
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Git command failed
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
# Git command failed
|
||||||
}
|
}
|
||||||
|
|
||||||
# For non-git repos, try to find the latest feature directory
|
# For non-git repos, try to find the latest feature directory
|
||||||
|
$repoRoot = Get-RepoRoot
|
||||||
$specsDir = Join-Path $repoRoot "specs"
|
$specsDir = Join-Path $repoRoot "specs"
|
||||||
|
|
||||||
if (Test-Path $specsDir) {
|
if (Test-Path $specsDir) {
|
||||||
$latestFeature = ""
|
$latestFeature = ""
|
||||||
$highest = 0
|
$highest = 0
|
||||||
$latestTimestamp = ""
|
|
||||||
|
|
||||||
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
||||||
if ($_.Name -match '^(\d{8}-\d{6})-') {
|
if ($_.Name -match '^(\d{3})-') {
|
||||||
# Timestamp-based branch: compare lexicographically
|
$num = [int]$matches[1]
|
||||||
$ts = $matches[1]
|
|
||||||
if ($ts -gt $latestTimestamp) {
|
|
||||||
$latestTimestamp = $ts
|
|
||||||
$latestFeature = $_.Name
|
|
||||||
}
|
|
||||||
} elseif ($_.Name -match '^(\d{3,})-') {
|
|
||||||
$num = [long]$matches[1]
|
|
||||||
if ($num -gt $highest) {
|
if ($num -gt $highest) {
|
||||||
$highest = $num
|
$highest = $num
|
||||||
# Only update if no timestamp branch found yet
|
$latestFeature = $_.Name
|
||||||
if (-not $latestTimestamp) {
|
|
||||||
$latestFeature = $_.Name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($latestFeature) {
|
if ($latestFeature) {
|
||||||
return $latestFeature
|
return $latestFeature
|
||||||
}
|
}
|
||||||
@@ -104,23 +58,9 @@ function Get-CurrentBranch {
|
|||||||
return "main"
|
return "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if we have git available at the spec-kit root level
|
|
||||||
# Returns true only if git is installed and the repo root is inside a git work tree
|
|
||||||
# Handles both regular repos (.git directory) and worktrees/submodules (.git file)
|
|
||||||
function Test-HasGit {
|
function Test-HasGit {
|
||||||
# First check if git command is available (before calling Get-RepoRoot which may use git)
|
|
||||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
$repoRoot = Get-RepoRoot
|
|
||||||
# Check if .git exists (directory or file for worktrees/submodules)
|
|
||||||
# Use -LiteralPath to handle paths with wildcard characters
|
|
||||||
if (-not (Test-Path -LiteralPath (Join-Path $repoRoot ".git"))) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
# Verify it's actually a valid git work tree
|
|
||||||
try {
|
try {
|
||||||
$null = git -C $repoRoot rev-parse --is-inside-work-tree 2>$null
|
git rev-parse --show-toplevel 2>$null | Out-Null
|
||||||
return ($LASTEXITCODE -eq 0)
|
return ($LASTEXITCODE -eq 0)
|
||||||
} catch {
|
} catch {
|
||||||
return $false
|
return $false
|
||||||
@@ -139,13 +79,9 @@ function Test-FeatureBranch {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Accept sequential prefix (3+ digits) but exclude malformed timestamps
|
if ($Branch -notmatch '^[0-9]{3}-') {
|
||||||
# Malformed: 7-or-8 digit date + 6-digit time with no trailing slug (e.g. "2026031-143022" or "20260319-143022")
|
|
||||||
$hasMalformedTimestamp = ($Branch -match '^[0-9]{7}-[0-9]{6}-') -or ($Branch -match '^(?:\d{7}|\d{8})-\d{6}$')
|
|
||||||
$isSequential = ($Branch -match '^[0-9]{3,}-') -and (-not $hasMalformedTimestamp)
|
|
||||||
if (-not $isSequential -and $Branch -notmatch '^\d{8}-\d{6}-') {
|
|
||||||
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
|
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
|
||||||
Write-Output "Feature branches should be named like: 001-feature-name, 1234-feature-name, or 20260319-143022-feature-name"
|
Write-Output "Feature branches should be named like: 001-feature-name"
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
return $true
|
return $true
|
||||||
@@ -199,70 +135,3 @@ function Test-DirHasFiles {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Resolve a template name to a file path using the priority stack:
|
|
||||||
# 1. .specify/templates/overrides/
|
|
||||||
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
|
|
||||||
# 3. .specify/extensions/<ext-id>/templates/
|
|
||||||
# 4. .specify/templates/ (core)
|
|
||||||
function Resolve-Template {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory=$true)][string]$TemplateName,
|
|
||||||
[Parameter(Mandatory=$true)][string]$RepoRoot
|
|
||||||
)
|
|
||||||
|
|
||||||
$base = Join-Path $RepoRoot '.specify/templates'
|
|
||||||
|
|
||||||
# Priority 1: Project overrides
|
|
||||||
$override = Join-Path $base "overrides/$TemplateName.md"
|
|
||||||
if (Test-Path $override) { return $override }
|
|
||||||
|
|
||||||
# Priority 2: Installed presets (sorted by priority from .registry)
|
|
||||||
$presetsDir = Join-Path $RepoRoot '.specify/presets'
|
|
||||||
if (Test-Path $presetsDir) {
|
|
||||||
$registryFile = Join-Path $presetsDir '.registry'
|
|
||||||
$sortedPresets = @()
|
|
||||||
if (Test-Path $registryFile) {
|
|
||||||
try {
|
|
||||||
$registryData = Get-Content $registryFile -Raw | ConvertFrom-Json
|
|
||||||
$presets = $registryData.presets
|
|
||||||
if ($presets) {
|
|
||||||
$sortedPresets = $presets.PSObject.Properties |
|
|
||||||
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
|
|
||||||
ForEach-Object { $_.Name }
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Fallback: alphabetical directory order
|
|
||||||
$sortedPresets = @()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($sortedPresets.Count -gt 0) {
|
|
||||||
foreach ($presetId in $sortedPresets) {
|
|
||||||
$candidate = Join-Path $presetsDir "$presetId/templates/$TemplateName.md"
|
|
||||||
if (Test-Path $candidate) { return $candidate }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
# Fallback: alphabetical directory order
|
|
||||||
foreach ($preset in Get-ChildItem -Path $presetsDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' }) {
|
|
||||||
$candidate = Join-Path $preset.FullName "templates/$TemplateName.md"
|
|
||||||
if (Test-Path $candidate) { return $candidate }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Priority 3: Extension-provided templates
|
|
||||||
$extDir = Join-Path $RepoRoot '.specify/extensions'
|
|
||||||
if (Test-Path $extDir) {
|
|
||||||
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' } | Sort-Object Name) {
|
|
||||||
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
|
|
||||||
if (Test-Path $candidate) { return $candidate }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Priority 4: Core templates
|
|
||||||
$core = Join-Path $base "$TemplateName.md"
|
|
||||||
if (Test-Path $core) { return $core }
|
|
||||||
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,41 +3,33 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[switch]$Json,
|
[switch]$Json,
|
||||||
[switch]$AllowExistingBranch,
|
|
||||||
[switch]$DryRun,
|
|
||||||
[string]$ShortName,
|
[string]$ShortName,
|
||||||
[Parameter()]
|
[int]$Number = 0,
|
||||||
[long]$Number = 0,
|
|
||||||
[switch]$Timestamp,
|
|
||||||
[switch]$Help,
|
[switch]$Help,
|
||||||
[Parameter(Position = 0, ValueFromRemainingArguments = $true)]
|
[Parameter(ValueFromRemainingArguments = $true)]
|
||||||
[string[]]$FeatureDescription
|
[string[]]$FeatureDescription
|
||||||
)
|
)
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
# Show help if requested
|
# Show help if requested
|
||||||
if ($Help) {
|
if ($Help) {
|
||||||
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Options:"
|
Write-Host "Options:"
|
||||||
Write-Host " -Json Output in JSON format"
|
Write-Host " -Json Output in JSON format"
|
||||||
Write-Host " -DryRun Compute branch name and paths without creating branches, directories, or files"
|
|
||||||
Write-Host " -AllowExistingBranch Switch to branch if it already exists instead of failing"
|
|
||||||
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
||||||
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
||||||
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
|
||||||
Write-Host " -Help Show this help message"
|
Write-Host " -Help Show this help message"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Examples:"
|
Write-Host "Examples:"
|
||||||
Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'"
|
Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'"
|
||||||
Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'"
|
Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'"
|
||||||
Write-Host " ./create-new-feature.ps1 -Timestamp -ShortName 'user-auth' 'Add user authentication'"
|
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if feature description provided
|
# Check if feature description provided
|
||||||
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
||||||
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-DryRun] [-AllowExistingBranch] [-ShortName <name>] [-Number N] [-Timestamp] <feature description>"
|
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,35 +41,39 @@ if ([string]::IsNullOrWhiteSpace($featureDesc)) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-HighestNumberFromSpecs {
|
# Resolve repository root. Prefer git information when available, but fall back
|
||||||
param([string]$SpecsDir)
|
# to searching for repository markers so the workflow still functions in repositories that
|
||||||
|
# were initialized with --no-git.
|
||||||
[long]$highest = 0
|
function Find-RepositoryRoot {
|
||||||
if (Test-Path $SpecsDir) {
|
param(
|
||||||
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
[string]$StartDir,
|
||||||
# Match sequential prefixes (>=3 digits), but skip timestamp dirs.
|
[string[]]$Markers = @('.git', '.specify')
|
||||||
if ($_.Name -match '^(\d{3,})-' -and $_.Name -notmatch '^\d{8}-\d{6}-') {
|
)
|
||||||
[long]$num = 0
|
$current = Resolve-Path $StartDir
|
||||||
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
|
while ($true) {
|
||||||
$highest = $num
|
foreach ($marker in $Markers) {
|
||||||
}
|
if (Test-Path (Join-Path $current $marker)) {
|
||||||
|
return $current
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$parent = Split-Path $current -Parent
|
||||||
|
if ($parent -eq $current) {
|
||||||
|
# Reached filesystem root without finding markers
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$current = $parent
|
||||||
}
|
}
|
||||||
return $highest
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract the highest sequential feature number from a list of branch/ref names.
|
function Get-HighestNumberFromSpecs {
|
||||||
# Shared by Get-HighestNumberFromBranches and Get-HighestNumberFromRemoteRefs.
|
param([string]$SpecsDir)
|
||||||
function Get-HighestNumberFromNames {
|
|
||||||
param([string[]]$Names)
|
$highest = 0
|
||||||
|
if (Test-Path $SpecsDir) {
|
||||||
[long]$highest = 0
|
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
||||||
foreach ($name in $Names) {
|
if ($_.Name -match '^(\d+)') {
|
||||||
if ($name -match '^(\d{3,})-' -and $name -notmatch '^\d{8}-\d{6}-') {
|
$num = [int]$matches[1]
|
||||||
[long]$num = 0
|
if ($num -gt $highest) { $highest = $num }
|
||||||
if ([long]::TryParse($matches[1], [ref]$num) -and $num -gt $highest) {
|
|
||||||
$highest = $num
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,68 +82,44 @@ function Get-HighestNumberFromNames {
|
|||||||
|
|
||||||
function Get-HighestNumberFromBranches {
|
function Get-HighestNumberFromBranches {
|
||||||
param()
|
param()
|
||||||
|
|
||||||
|
$highest = 0
|
||||||
try {
|
try {
|
||||||
$branches = git branch -a 2>$null
|
$branches = git branch -a 2>$null
|
||||||
if ($LASTEXITCODE -eq 0 -and $branches) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$cleanNames = $branches | ForEach-Object {
|
foreach ($branch in $branches) {
|
||||||
$_.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
# Clean branch name: remove leading markers and remote prefixes
|
||||||
}
|
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
||||||
return Get-HighestNumberFromNames -Names $cleanNames
|
|
||||||
}
|
# Extract feature number if branch matches pattern ###-*
|
||||||
} catch {
|
if ($cleanBranch -match '^(\d+)-') {
|
||||||
Write-Verbose "Could not check Git branches: $_"
|
$num = [int]$matches[1]
|
||||||
}
|
if ($num -gt $highest) { $highest = $num }
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-HighestNumberFromRemoteRefs {
|
|
||||||
[long]$highest = 0
|
|
||||||
try {
|
|
||||||
$remotes = git remote 2>$null
|
|
||||||
if ($remotes) {
|
|
||||||
foreach ($remote in $remotes) {
|
|
||||||
$env:GIT_TERMINAL_PROMPT = '0'
|
|
||||||
$refs = git ls-remote --heads $remote 2>$null
|
|
||||||
$env:GIT_TERMINAL_PROMPT = $null
|
|
||||||
if ($LASTEXITCODE -eq 0 -and $refs) {
|
|
||||||
$refNames = $refs | ForEach-Object {
|
|
||||||
if ($_ -match 'refs/heads/(.+)$') { $matches[1] }
|
|
||||||
} | Where-Object { $_ }
|
|
||||||
$remoteHighest = Get-HighestNumberFromNames -Names $refNames
|
|
||||||
if ($remoteHighest -gt $highest) { $highest = $remoteHighest }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
Write-Verbose "Could not query remote refs: $_"
|
# If git command fails, return 0
|
||||||
|
Write-Verbose "Could not check Git branches: $_"
|
||||||
}
|
}
|
||||||
return $highest
|
return $highest
|
||||||
}
|
}
|
||||||
|
|
||||||
# Return next available branch number. When SkipFetch is true, queries remotes
|
|
||||||
# via ls-remote (read-only) instead of fetching.
|
|
||||||
function Get-NextBranchNumber {
|
function Get-NextBranchNumber {
|
||||||
param(
|
param(
|
||||||
[string]$SpecsDir,
|
[string]$SpecsDir
|
||||||
[switch]$SkipFetch
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if ($SkipFetch) {
|
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
||||||
# Side-effect-free: query remotes via ls-remote
|
try {
|
||||||
$highestBranch = Get-HighestNumberFromBranches
|
git fetch --all --prune 2>$null | Out-Null
|
||||||
$highestRemote = Get-HighestNumberFromRemoteRefs
|
} catch {
|
||||||
$highestBranch = [Math]::Max($highestBranch, $highestRemote)
|
# Ignore fetch errors
|
||||||
} else {
|
|
||||||
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
|
||||||
try {
|
|
||||||
git fetch --all --prune 2>$null | Out-Null
|
|
||||||
} catch {
|
|
||||||
# Ignore fetch errors
|
|
||||||
}
|
|
||||||
$highestBranch = Get-HighestNumberFromBranches
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get highest number from ALL branches (not just matching short name)
|
||||||
|
$highestBranch = Get-HighestNumberFromBranches
|
||||||
|
|
||||||
# Get highest number from ALL specs (not just matching short name)
|
# Get highest number from ALL specs (not just matching short name)
|
||||||
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
|
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
|
||||||
|
|
||||||
@@ -160,29 +132,36 @@ function Get-NextBranchNumber {
|
|||||||
|
|
||||||
function ConvertTo-CleanBranchName {
|
function ConvertTo-CleanBranchName {
|
||||||
param([string]$Name)
|
param([string]$Name)
|
||||||
|
|
||||||
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
||||||
}
|
}
|
||||||
# Load common functions (includes Get-RepoRoot, Test-HasGit, Resolve-Template)
|
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
|
||||||
. "$PSScriptRoot/common.ps1"
|
if (-not $fallbackRoot) {
|
||||||
|
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
# Use common.ps1 functions which prioritize .specify over git
|
try {
|
||||||
$repoRoot = Get-RepoRoot
|
$repoRoot = git rev-parse --show-toplevel 2>$null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
# Check if git is available at this repo root (not a parent)
|
$hasGit = $true
|
||||||
$hasGit = Test-HasGit
|
} else {
|
||||||
|
throw "Git not available"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$repoRoot = $fallbackRoot
|
||||||
|
$hasGit = $false
|
||||||
|
}
|
||||||
|
|
||||||
Set-Location $repoRoot
|
Set-Location $repoRoot
|
||||||
|
|
||||||
$specsDir = Join-Path $repoRoot 'specs'
|
$specsDir = Join-Path $repoRoot 'specs'
|
||||||
if (-not $DryRun) {
|
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
||||||
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to generate branch name with stop word filtering and length filtering
|
# Function to generate branch name with stop word filtering and length filtering
|
||||||
function Get-BranchName {
|
function Get-BranchName {
|
||||||
param([string]$Description)
|
param([string]$Description)
|
||||||
|
|
||||||
# Common stop words to filter out
|
# Common stop words to filter out
|
||||||
$stopWords = @(
|
$stopWords = @(
|
||||||
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
|
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
|
||||||
@@ -191,17 +170,17 @@ function Get-BranchName {
|
|||||||
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
|
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
|
||||||
'want', 'need', 'add', 'get', 'set'
|
'want', 'need', 'add', 'get', 'set'
|
||||||
)
|
)
|
||||||
|
|
||||||
# Convert to lowercase and extract words (alphanumeric only)
|
# Convert to lowercase and extract words (alphanumeric only)
|
||||||
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
|
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
|
||||||
$words = $cleanName -split '\s+' | Where-Object { $_ }
|
$words = $cleanName -split '\s+' | Where-Object { $_ }
|
||||||
|
|
||||||
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
||||||
$meaningfulWords = @()
|
$meaningfulWords = @()
|
||||||
foreach ($word in $words) {
|
foreach ($word in $words) {
|
||||||
# Skip stop words
|
# Skip stop words
|
||||||
if ($stopWords -contains $word) { continue }
|
if ($stopWords -contains $word) { continue }
|
||||||
|
|
||||||
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
|
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
|
||||||
if ($word.Length -ge 3) {
|
if ($word.Length -ge 3) {
|
||||||
$meaningfulWords += $word
|
$meaningfulWords += $word
|
||||||
@@ -210,7 +189,7 @@ function Get-BranchName {
|
|||||||
$meaningfulWords += $word
|
$meaningfulWords += $word
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# If we have meaningful words, use first 3-4 of them
|
# If we have meaningful words, use first 3-4 of them
|
||||||
if ($meaningfulWords.Count -gt 0) {
|
if ($meaningfulWords.Count -gt 0) {
|
||||||
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
|
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
|
||||||
@@ -233,134 +212,94 @@ if ($ShortName) {
|
|||||||
$branchSuffix = Get-BranchName -Description $featureDesc
|
$branchSuffix = Get-BranchName -Description $featureDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
# Warn if -Number and -Timestamp are both specified
|
# Determine branch number
|
||||||
if ($Timestamp -and $Number -ne 0) {
|
if ($Number -eq 0) {
|
||||||
Write-Warning "[specify] Warning: -Number is ignored when -Timestamp is used"
|
if ($hasGit) {
|
||||||
$Number = 0
|
# Check existing branches on remotes
|
||||||
}
|
$Number = Get-NextBranchNumber -SpecsDir $specsDir
|
||||||
|
} else {
|
||||||
# Determine branch prefix
|
# Fall back to local directory check
|
||||||
if ($Timestamp) {
|
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||||
$featureNum = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
||||||
$branchName = "$featureNum-$branchSuffix"
|
|
||||||
} else {
|
|
||||||
# Determine branch number
|
|
||||||
if ($Number -eq 0) {
|
|
||||||
if ($DryRun -and $hasGit) {
|
|
||||||
# Dry-run: query remotes via ls-remote (side-effect-free, no fetch)
|
|
||||||
$Number = Get-NextBranchNumber -SpecsDir $specsDir -SkipFetch
|
|
||||||
} elseif ($DryRun) {
|
|
||||||
# Dry-run without git: local spec dirs only
|
|
||||||
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
|
||||||
} elseif ($hasGit) {
|
|
||||||
# Check existing branches on remotes
|
|
||||||
$Number = Get-NextBranchNumber -SpecsDir $specsDir
|
|
||||||
} else {
|
|
||||||
# Fall back to local directory check
|
|
||||||
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$featureNum = ('{0:000}' -f $Number)
|
|
||||||
$branchName = "$featureNum-$branchSuffix"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$featureNum = ('{0:000}' -f $Number)
|
||||||
|
$branchName = "$featureNum-$branchSuffix"
|
||||||
|
|
||||||
# GitHub enforces a 244-byte limit on branch names
|
# GitHub enforces a 244-byte limit on branch names
|
||||||
# Validate and truncate if necessary
|
# Validate and truncate if necessary
|
||||||
$maxBranchLength = 244
|
$maxBranchLength = 244
|
||||||
if ($branchName.Length -gt $maxBranchLength) {
|
if ($branchName.Length -gt $maxBranchLength) {
|
||||||
# Calculate how much we need to trim from suffix
|
# Calculate how much we need to trim from suffix
|
||||||
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
|
# Account for: feature number (3) + hyphen (1) = 4 chars
|
||||||
$prefixLength = $featureNum.Length + 1
|
$maxSuffixLength = $maxBranchLength - 4
|
||||||
$maxSuffixLength = $maxBranchLength - $prefixLength
|
|
||||||
|
|
||||||
# Truncate suffix
|
# Truncate suffix
|
||||||
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
||||||
# Remove trailing hyphen if truncation created one
|
# Remove trailing hyphen if truncation created one
|
||||||
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
||||||
|
|
||||||
$originalBranchName = $branchName
|
$originalBranchName = $branchName
|
||||||
$branchName = "$featureNum-$truncatedSuffix"
|
$branchName = "$featureNum-$truncatedSuffix"
|
||||||
|
|
||||||
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
|
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
|
||||||
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
|
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
|
||||||
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
|
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
|
||||||
}
|
}
|
||||||
|
|
||||||
$featureDir = Join-Path $specsDir $branchName
|
if ($hasGit) {
|
||||||
$specFile = Join-Path $featureDir 'spec.md'
|
$branchCreated = $false
|
||||||
|
try {
|
||||||
if (-not $DryRun) {
|
git checkout -b $branchName 2>$null | Out-Null
|
||||||
if ($hasGit) {
|
if ($LASTEXITCODE -eq 0) {
|
||||||
$branchCreated = $false
|
$branchCreated = $true
|
||||||
try {
|
|
||||||
git checkout -q -b $branchName 2>$null | Out-Null
|
|
||||||
if ($LASTEXITCODE -eq 0) {
|
|
||||||
$branchCreated = $true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
# Exception during git command
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
if (-not $branchCreated) {
|
# Exception during git command
|
||||||
# Check if branch already exists
|
|
||||||
$existingBranch = git branch --list $branchName 2>$null
|
|
||||||
if ($existingBranch) {
|
|
||||||
if ($AllowExistingBranch) {
|
|
||||||
# Switch to the existing branch instead of failing
|
|
||||||
git checkout -q $branchName 2>$null | Out-Null
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
Write-Error "Error: Branch '$branchName' exists but could not be checked out. Resolve any uncommitted changes or conflicts and try again."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} elseif ($Timestamp) {
|
|
||||||
Write-Error "Error: Branch '$branchName' already exists. Rerun to get a new timestamp or use a different -ShortName."
|
|
||||||
exit 1
|
|
||||||
} else {
|
|
||||||
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
if (-not $branchCreated) {
|
||||||
|
# Check if branch already exists
|
||||||
if (-not (Test-Path -PathType Leaf $specFile)) {
|
$existingBranch = git branch --list $branchName 2>$null
|
||||||
$template = Resolve-Template -TemplateName 'spec-template' -RepoRoot $repoRoot
|
if ($existingBranch) {
|
||||||
if ($template -and (Test-Path $template)) {
|
Write-Error "Error: Branch '$branchName' already exists. Please use a different feature name or specify a different number with -Number."
|
||||||
Copy-Item $template $specFile -Force
|
exit 1
|
||||||
} else {
|
} else {
|
||||||
New-Item -ItemType File -Path $specFile -Force | Out-Null
|
Write-Error "Error: Failed to create git branch '$branchName'. Please check your git configuration and try again."
|
||||||
|
exit 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
# Set the SPECIFY_FEATURE environment variable for the current session
|
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
|
||||||
$env:SPECIFY_FEATURE = $branchName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$featureDir = Join-Path $specsDir $branchName
|
||||||
|
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
||||||
|
|
||||||
|
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
|
||||||
|
$specFile = Join-Path $featureDir 'spec.md'
|
||||||
|
if (Test-Path $template) {
|
||||||
|
Copy-Item $template $specFile -Force
|
||||||
|
} else {
|
||||||
|
New-Item -ItemType File -Path $specFile | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set the SPECIFY_FEATURE environment variable for the current session
|
||||||
|
$env:SPECIFY_FEATURE = $branchName
|
||||||
|
|
||||||
if ($Json) {
|
if ($Json) {
|
||||||
$obj = [PSCustomObject]@{
|
$obj = [PSCustomObject]@{
|
||||||
BRANCH_NAME = $branchName
|
BRANCH_NAME = $branchName
|
||||||
SPEC_FILE = $specFile
|
SPEC_FILE = $specFile
|
||||||
FEATURE_NUM = $featureNum
|
FEATURE_NUM = $featureNum
|
||||||
HAS_GIT = $hasGit
|
HAS_GIT = $hasGit
|
||||||
}
|
}
|
||||||
if ($DryRun) {
|
|
||||||
$obj | Add-Member -NotePropertyName 'DRY_RUN' -NotePropertyValue $true
|
|
||||||
}
|
|
||||||
$obj | ConvertTo-Json -Compress
|
$obj | ConvertTo-Json -Compress
|
||||||
} else {
|
} else {
|
||||||
Write-Output "BRANCH_NAME: $branchName"
|
Write-Output "BRANCH_NAME: $branchName"
|
||||||
Write-Output "SPEC_FILE: $specFile"
|
Write-Output "SPEC_FILE: $specFile"
|
||||||
Write-Output "FEATURE_NUM: $featureNum"
|
Write-Output "FEATURE_NUM: $featureNum"
|
||||||
Write-Output "HAS_GIT: $hasGit"
|
Write-Output "HAS_GIT: $hasGit"
|
||||||
if (-not $DryRun) {
|
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
||||||
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,12 @@ if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GI
|
|||||||
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
|
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
|
||||||
|
|
||||||
# Copy plan template if it exists, otherwise note it or create empty file
|
# Copy plan template if it exists, otherwise note it or create empty file
|
||||||
$template = Resolve-Template -TemplateName 'plan-template' -RepoRoot $paths.REPO_ROOT
|
$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md'
|
||||||
if ($template -and (Test-Path $template)) {
|
if (Test-Path $template) {
|
||||||
Copy-Item $template $paths.IMPL_PLAN -Force
|
Copy-Item $template $paths.IMPL_PLAN -Force
|
||||||
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
|
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
|
||||||
} else {
|
} else {
|
||||||
Write-Warning "Plan template not found"
|
Write-Warning "Plan template not found at $template"
|
||||||
# Create a basic plan file if template doesn't exist
|
# Create a basic plan file if template doesn't exist
|
||||||
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
|
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Mirrors the behavior of scripts/bash/update-agent-context.sh:
|
|||||||
2. Plan Data Extraction
|
2. Plan Data Extraction
|
||||||
3. Agent File Management (create from template or update existing)
|
3. Agent File Management (create from template or update existing)
|
||||||
4. Content Generation (technology stack, recent changes, timestamp)
|
4. Content Generation (technology stack, recent changes, timestamp)
|
||||||
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, junie, kilocode, auggie, roo, codebuddy, amp, shai, tabnine, kiro-cli, agy, bob, vibe, qodercli, kimi, trae, pi, iflow, generic)
|
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, kiro-cli, agy, bob, qodercli)
|
||||||
|
|
||||||
.PARAMETER AgentType
|
.PARAMETER AgentType
|
||||||
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
|
Optional agent key to update a single agent. If omitted, updates all existing agent files (creating a default Claude file if none exist).
|
||||||
@@ -25,7 +25,7 @@ Relies on common helper functions in common.ps1
|
|||||||
#>
|
#>
|
||||||
param(
|
param(
|
||||||
[Parameter(Position=0)]
|
[Parameter(Position=0)]
|
||||||
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','junie','kilocode','auggie','roo','codebuddy','amp','shai','tabnine','kiro-cli','agy','bob','qodercli','vibe','kimi','trae','pi','iflow','generic')]
|
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','kiro-cli','agy','bob','qodercli','generic')]
|
||||||
[string]$AgentType
|
[string]$AgentType
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -46,12 +46,11 @@ $NEW_PLAN = $IMPL_PLAN
|
|||||||
# Agent file paths
|
# Agent file paths
|
||||||
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
|
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
|
||||||
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
|
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
|
||||||
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/copilot-instructions.md'
|
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md'
|
||||||
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
|
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
|
||||||
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
|
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
|
||||||
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/rules/specify-rules.md'
|
$WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/rules/specify-rules.md'
|
||||||
$JUNIE_FILE = Join-Path $REPO_ROOT '.junie/AGENTS.md'
|
|
||||||
$KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md'
|
$KILOCODE_FILE = Join-Path $REPO_ROOT '.kilocode/rules/specify-rules.md'
|
||||||
$AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md'
|
$AUGGIE_FILE = Join-Path $REPO_ROOT '.augment/rules/specify-rules.md'
|
||||||
$ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md'
|
$ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md'
|
||||||
@@ -59,14 +58,9 @@ $CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md'
|
|||||||
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
|
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
|
||||||
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
|
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
|
||||||
$TABNINE_FILE = Join-Path $REPO_ROOT 'TABNINE.md'
|
|
||||||
$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$KIRO_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
|
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
|
||||||
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
|
||||||
$VIBE_FILE = Join-Path $REPO_ROOT '.vibe/agents/specify-agents.md'
|
|
||||||
$KIMI_FILE = Join-Path $REPO_ROOT 'KIMI.md'
|
|
||||||
$TRAE_FILE = Join-Path $REPO_ROOT '.trae/rules/AGENTS.md'
|
|
||||||
$IFLOW_FILE = Join-Path $REPO_ROOT 'IFLOW.md'
|
|
||||||
|
|
||||||
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
|
||||||
|
|
||||||
@@ -334,7 +328,7 @@ function Update-ExistingAgentFile {
|
|||||||
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
|
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ($line -match '(\*\*)?Last updated(\*\*)?: .*\d{4}-\d{2}-\d{2}') {
|
if ($line -match '\*\*Last updated\*\*: .*\d{4}-\d{2}-\d{2}') {
|
||||||
$output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd')))
|
$output.Add(($line -replace '\d{4}-\d{2}-\d{2}',$Date.ToString('yyyy-MM-dd')))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -398,7 +392,6 @@ function Update-SpecificAgent {
|
|||||||
'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' }
|
'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' }
|
||||||
'codex' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex CLI' }
|
'codex' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex CLI' }
|
||||||
'windsurf' { Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf' }
|
'windsurf' { Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf' }
|
||||||
'junie' { Update-AgentFile -TargetFile $JUNIE_FILE -AgentName 'Junie' }
|
|
||||||
'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' }
|
'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' }
|
||||||
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
|
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
|
||||||
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
|
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
|
||||||
@@ -406,17 +399,11 @@ function Update-SpecificAgent {
|
|||||||
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
|
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
|
||||||
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
|
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
|
||||||
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
|
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
|
||||||
'tabnine' { Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI' }
|
|
||||||
'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' }
|
'kiro-cli' { Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI' }
|
||||||
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
|
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
|
||||||
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
|
||||||
'vibe' { Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe' }
|
|
||||||
'kimi' { Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code' }
|
|
||||||
'trae' { Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae' }
|
|
||||||
'pi' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Pi Coding Agent' }
|
|
||||||
'iflow' { Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI' }
|
|
||||||
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
'generic' { Write-Info 'Generic agent: no predefined context file. Use the agent-specific update script for your agent.' }
|
||||||
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic'; return $false }
|
default { Write-Err "Unknown agent type '$Type'"; Write-Err 'Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic'; return $false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,21 +417,15 @@ function Update-AllExistingAgents {
|
|||||||
if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true }
|
if (Test-Path $QWEN_FILE) { if (-not (Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true }
|
if (Test-Path $AGENTS_FILE) { if (-not (Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex/opencode')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $WINDSURF_FILE) { if (-not (Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }; $found = $true }
|
if (Test-Path $WINDSURF_FILE) { if (-not (Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $JUNIE_FILE) { if (-not (Update-AgentFile -TargetFile $JUNIE_FILE -AgentName 'Junie')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $KILOCODE_FILE) { if (-not (Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }; $found = $true }
|
if (Test-Path $KILOCODE_FILE) { if (-not (Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $AUGGIE_FILE) { if (-not (Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true }
|
if (Test-Path $ROO_FILE) { if (-not (Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $CODEBUDDY_FILE) { if (-not (Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $QODER_FILE) { if (-not (Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
if (Test-Path $SHAI_FILE) { if (-not (Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $TABNINE_FILE) { if (-not (Update-AgentFile -TargetFile $TABNINE_FILE -AgentName 'Tabnine CLI')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true }
|
if (Test-Path $KIRO_FILE) { if (-not (Update-AgentFile -TargetFile $KIRO_FILE -AgentName 'Kiro CLI')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
|
if (Test-Path $AGY_FILE) { if (-not (Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
|
if (Test-Path $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
|
||||||
if (Test-Path $VIBE_FILE) { if (-not (Update-AgentFile -TargetFile $VIBE_FILE -AgentName 'Mistral Vibe')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $KIMI_FILE) { if (-not (Update-AgentFile -TargetFile $KIMI_FILE -AgentName 'Kimi Code')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $TRAE_FILE) { if (-not (Update-AgentFile -TargetFile $TRAE_FILE -AgentName 'Trae')) { $ok = $false }; $found = $true }
|
|
||||||
if (Test-Path $IFLOW_FILE) { if (-not (Update-AgentFile -TargetFile $IFLOW_FILE -AgentName 'iFlow CLI')) { $ok = $false }; $found = $true }
|
|
||||||
if (-not $found) {
|
if (-not $found) {
|
||||||
Write-Info 'No existing agent files found, creating default Claude file...'
|
Write-Info 'No existing agent files found, creating default Claude file...'
|
||||||
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
|
||||||
@@ -459,7 +440,7 @@ function Print-Summary {
|
|||||||
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
|
||||||
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
|
||||||
Write-Host ''
|
Write-Host ''
|
||||||
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|junie|kilocode|auggie|roo|codebuddy|amp|shai|tabnine|kiro-cli|agy|bob|vibe|qodercli|kimi|trae|pi|iflow|generic]'
|
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|kiro-cli|agy|bob|qodercli|generic]'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Main {
|
function Main {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,543 +0,0 @@
|
|||||||
"""
|
|
||||||
Agent Command Registrar for Spec Kit
|
|
||||||
|
|
||||||
Shared infrastructure for registering commands with AI agents.
|
|
||||||
Used by both the extension system and the preset system to write
|
|
||||||
command files into agent-specific directories in the correct format.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict, List, Any
|
|
||||||
|
|
||||||
import platform
|
|
||||||
import re
|
|
||||||
from copy import deepcopy
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
|
|
||||||
def _build_agent_configs() -> dict[str, Any]:
|
|
||||||
"""Derive CommandRegistrar.AGENT_CONFIGS from INTEGRATION_REGISTRY."""
|
|
||||||
from specify_cli.integrations import INTEGRATION_REGISTRY
|
|
||||||
configs: dict[str, dict[str, Any]] = {}
|
|
||||||
for key, integration in INTEGRATION_REGISTRY.items():
|
|
||||||
if key == "generic":
|
|
||||||
continue
|
|
||||||
if integration.registrar_config:
|
|
||||||
configs[key] = dict(integration.registrar_config)
|
|
||||||
return configs
|
|
||||||
|
|
||||||
|
|
||||||
class CommandRegistrar:
|
|
||||||
"""Handles registration of commands with AI agents.
|
|
||||||
|
|
||||||
Supports writing command files in Markdown or TOML format to the
|
|
||||||
appropriate agent directory, with correct argument placeholders
|
|
||||||
and companion files (e.g. Copilot .prompt.md).
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Derived from INTEGRATION_REGISTRY — single source of truth.
|
|
||||||
# Populated lazily via _ensure_configs() on first use.
|
|
||||||
AGENT_CONFIGS: dict[str, dict[str, Any]] = {}
|
|
||||||
_configs_loaded: bool = False
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self._ensure_configs()
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
cls._ensure_configs()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _ensure_configs(cls) -> None:
|
|
||||||
if not cls._configs_loaded:
|
|
||||||
try:
|
|
||||||
cls.AGENT_CONFIGS = _build_agent_configs()
|
|
||||||
cls._configs_loaded = True
|
|
||||||
except ImportError:
|
|
||||||
pass # Circular import during module init; retry on next access
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_frontmatter(content: str) -> tuple[dict, str]:
|
|
||||||
"""Parse YAML frontmatter from Markdown content.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Markdown content with YAML frontmatter
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (frontmatter_dict, body_content)
|
|
||||||
"""
|
|
||||||
if not content.startswith("---"):
|
|
||||||
return {}, content
|
|
||||||
|
|
||||||
# Find second ---
|
|
||||||
end_marker = content.find("---", 3)
|
|
||||||
if end_marker == -1:
|
|
||||||
return {}, content
|
|
||||||
|
|
||||||
frontmatter_str = content[3:end_marker].strip()
|
|
||||||
body = content[end_marker + 3:].strip()
|
|
||||||
|
|
||||||
try:
|
|
||||||
frontmatter = yaml.safe_load(frontmatter_str) or {}
|
|
||||||
except yaml.YAMLError:
|
|
||||||
frontmatter = {}
|
|
||||||
|
|
||||||
if not isinstance(frontmatter, dict):
|
|
||||||
frontmatter = {}
|
|
||||||
|
|
||||||
return frontmatter, body
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def render_frontmatter(fm: dict) -> str:
|
|
||||||
"""Render frontmatter dictionary as YAML.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fm: Frontmatter dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
YAML-formatted frontmatter with delimiters
|
|
||||||
"""
|
|
||||||
if not fm:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
yaml_str = yaml.dump(fm, default_flow_style=False, sort_keys=False, allow_unicode=True)
|
|
||||||
return f"---\n{yaml_str}---\n"
|
|
||||||
|
|
||||||
def _adjust_script_paths(self, frontmatter: dict) -> dict:
|
|
||||||
"""Normalize script paths in frontmatter to generated project locations.
|
|
||||||
|
|
||||||
Rewrites known repo-relative and top-level script paths under the
|
|
||||||
`scripts` and `agent_scripts` keys (for example `../../scripts/`,
|
|
||||||
`../../templates/`, `../../memory/`, `scripts/`, `templates/`, and
|
|
||||||
`memory/`) to the `.specify/...` paths used in generated projects.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frontmatter: Frontmatter dictionary
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Modified frontmatter with normalized project paths
|
|
||||||
"""
|
|
||||||
frontmatter = deepcopy(frontmatter)
|
|
||||||
|
|
||||||
for script_key in ("scripts", "agent_scripts"):
|
|
||||||
scripts = frontmatter.get(script_key)
|
|
||||||
if not isinstance(scripts, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for key, script_path in scripts.items():
|
|
||||||
if isinstance(script_path, str):
|
|
||||||
scripts[key] = self.rewrite_project_relative_paths(script_path)
|
|
||||||
return frontmatter
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def rewrite_project_relative_paths(text: str) -> str:
|
|
||||||
"""Rewrite repo-relative paths to their generated project locations."""
|
|
||||||
if not isinstance(text, str) or not text:
|
|
||||||
return text
|
|
||||||
|
|
||||||
for old, new in (
|
|
||||||
("../../memory/", ".specify/memory/"),
|
|
||||||
("../../scripts/", ".specify/scripts/"),
|
|
||||||
("../../templates/", ".specify/templates/"),
|
|
||||||
):
|
|
||||||
text = text.replace(old, new)
|
|
||||||
|
|
||||||
# Only rewrite top-level style references so extension-local paths like
|
|
||||||
# ".specify/extensions/<ext>/scripts/..." remain intact.
|
|
||||||
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?memory/', r"\1.specify/memory/", text)
|
|
||||||
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?scripts/', r"\1.specify/scripts/", text)
|
|
||||||
text = re.sub(r'(^|[\s`"\'(])(?:\.?/)?templates/', r"\1.specify/templates/", text)
|
|
||||||
|
|
||||||
return text.replace(".specify/.specify/", ".specify/").replace(".specify.specify/", ".specify/")
|
|
||||||
|
|
||||||
def render_markdown_command(
|
|
||||||
self,
|
|
||||||
frontmatter: dict,
|
|
||||||
body: str,
|
|
||||||
source_id: str,
|
|
||||||
context_note: str = None
|
|
||||||
) -> str:
|
|
||||||
"""Render command in Markdown format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frontmatter: Command frontmatter
|
|
||||||
body: Command body content
|
|
||||||
source_id: Source identifier (extension or preset ID)
|
|
||||||
context_note: Custom context comment (default: <!-- Source: {source_id} -->)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Formatted Markdown command file content
|
|
||||||
"""
|
|
||||||
if context_note is None:
|
|
||||||
context_note = f"\n<!-- Source: {source_id} -->\n"
|
|
||||||
return self.render_frontmatter(frontmatter) + "\n" + context_note + body
|
|
||||||
|
|
||||||
def render_toml_command(
|
|
||||||
self,
|
|
||||||
frontmatter: dict,
|
|
||||||
body: str,
|
|
||||||
source_id: str
|
|
||||||
) -> str:
|
|
||||||
"""Render command in TOML format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frontmatter: Command frontmatter
|
|
||||||
body: Command body content
|
|
||||||
source_id: Source identifier (extension or preset ID)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Formatted TOML command file content
|
|
||||||
"""
|
|
||||||
toml_lines = []
|
|
||||||
|
|
||||||
if "description" in frontmatter:
|
|
||||||
desc = frontmatter["description"].replace('"', '\\"')
|
|
||||||
toml_lines.append(f'description = "{desc}"')
|
|
||||||
toml_lines.append("")
|
|
||||||
|
|
||||||
toml_lines.append(f"# Source: {source_id}")
|
|
||||||
toml_lines.append("")
|
|
||||||
|
|
||||||
# Keep TOML output valid even when body contains triple-quote delimiters.
|
|
||||||
# Prefer multiline forms, then fall back to escaped basic string.
|
|
||||||
if '"""' not in body:
|
|
||||||
toml_lines.append('prompt = """')
|
|
||||||
toml_lines.append(body)
|
|
||||||
toml_lines.append('"""')
|
|
||||||
elif "'''" not in body:
|
|
||||||
toml_lines.append("prompt = '''")
|
|
||||||
toml_lines.append(body)
|
|
||||||
toml_lines.append("'''")
|
|
||||||
else:
|
|
||||||
escaped_body = (
|
|
||||||
body.replace("\\", "\\\\")
|
|
||||||
.replace('"', '\\"')
|
|
||||||
.replace("\n", "\\n")
|
|
||||||
.replace("\r", "\\r")
|
|
||||||
.replace("\t", "\\t")
|
|
||||||
)
|
|
||||||
toml_lines.append(f'prompt = "{escaped_body}"')
|
|
||||||
|
|
||||||
return "\n".join(toml_lines)
|
|
||||||
|
|
||||||
def render_skill_command(
|
|
||||||
self,
|
|
||||||
agent_name: str,
|
|
||||||
skill_name: str,
|
|
||||||
frontmatter: dict,
|
|
||||||
body: str,
|
|
||||||
source_id: str,
|
|
||||||
source_file: str,
|
|
||||||
project_root: Path,
|
|
||||||
) -> str:
|
|
||||||
"""Render a command override as a SKILL.md file.
|
|
||||||
|
|
||||||
SKILL-target agents should receive the same skills-oriented
|
|
||||||
frontmatter shape used elsewhere in the project instead of the
|
|
||||||
original command frontmatter.
|
|
||||||
|
|
||||||
Technical debt note:
|
|
||||||
Spec-kit currently has multiple SKILL.md generators (template packaging,
|
|
||||||
init-time conversion, and extension/preset overrides). Keep the skill
|
|
||||||
frontmatter keys aligned (name/description/compatibility/metadata, with
|
|
||||||
metadata.author and metadata.source subkeys) to avoid drift across agents.
|
|
||||||
"""
|
|
||||||
if not isinstance(frontmatter, dict):
|
|
||||||
frontmatter = {}
|
|
||||||
|
|
||||||
if agent_name in {"codex", "kimi"}:
|
|
||||||
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
|
|
||||||
|
|
||||||
description = frontmatter.get("description", f"Spec-kit workflow command: {skill_name}")
|
|
||||||
skill_frontmatter = self.build_skill_frontmatter(
|
|
||||||
agent_name,
|
|
||||||
skill_name,
|
|
||||||
description,
|
|
||||||
f"{source_id}:{source_file}",
|
|
||||||
)
|
|
||||||
return self.render_frontmatter(skill_frontmatter) + "\n" + body
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def build_skill_frontmatter(
|
|
||||||
agent_name: str,
|
|
||||||
skill_name: str,
|
|
||||||
description: str,
|
|
||||||
source: str,
|
|
||||||
) -> dict:
|
|
||||||
"""Build consistent SKILL.md frontmatter across all skill generators."""
|
|
||||||
skill_frontmatter = {
|
|
||||||
"name": skill_name,
|
|
||||||
"description": description,
|
|
||||||
"compatibility": "Requires spec-kit project structure with .specify/ directory",
|
|
||||||
"metadata": {
|
|
||||||
"author": "github-spec-kit",
|
|
||||||
"source": source,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if agent_name == "claude":
|
|
||||||
# Claude skills should only run when explicitly invoked.
|
|
||||||
skill_frontmatter["disable-model-invocation"] = True
|
|
||||||
return skill_frontmatter
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_skill_placeholders(agent_name: str, frontmatter: dict, body: str, project_root: Path) -> str:
|
|
||||||
"""Resolve script placeholders for skills-backed agents."""
|
|
||||||
try:
|
|
||||||
from . import load_init_options
|
|
||||||
except ImportError:
|
|
||||||
return body
|
|
||||||
|
|
||||||
if not isinstance(frontmatter, dict):
|
|
||||||
frontmatter = {}
|
|
||||||
|
|
||||||
scripts = frontmatter.get("scripts", {}) or {}
|
|
||||||
agent_scripts = frontmatter.get("agent_scripts", {}) or {}
|
|
||||||
if not isinstance(scripts, dict):
|
|
||||||
scripts = {}
|
|
||||||
if not isinstance(agent_scripts, dict):
|
|
||||||
agent_scripts = {}
|
|
||||||
|
|
||||||
init_opts = load_init_options(project_root)
|
|
||||||
if not isinstance(init_opts, dict):
|
|
||||||
init_opts = {}
|
|
||||||
|
|
||||||
script_variant = init_opts.get("script")
|
|
||||||
if script_variant not in {"sh", "ps"}:
|
|
||||||
fallback_order = []
|
|
||||||
default_variant = "ps" if platform.system().lower().startswith("win") else "sh"
|
|
||||||
secondary_variant = "sh" if default_variant == "ps" else "ps"
|
|
||||||
|
|
||||||
if default_variant in scripts or default_variant in agent_scripts:
|
|
||||||
fallback_order.append(default_variant)
|
|
||||||
if secondary_variant in scripts or secondary_variant in agent_scripts:
|
|
||||||
fallback_order.append(secondary_variant)
|
|
||||||
|
|
||||||
for key in scripts:
|
|
||||||
if key not in fallback_order:
|
|
||||||
fallback_order.append(key)
|
|
||||||
for key in agent_scripts:
|
|
||||||
if key not in fallback_order:
|
|
||||||
fallback_order.append(key)
|
|
||||||
|
|
||||||
script_variant = fallback_order[0] if fallback_order else None
|
|
||||||
|
|
||||||
script_command = scripts.get(script_variant) if script_variant else None
|
|
||||||
if script_command:
|
|
||||||
script_command = script_command.replace("{ARGS}", "$ARGUMENTS")
|
|
||||||
body = body.replace("{SCRIPT}", script_command)
|
|
||||||
|
|
||||||
agent_script_command = agent_scripts.get(script_variant) if script_variant else None
|
|
||||||
if agent_script_command:
|
|
||||||
agent_script_command = agent_script_command.replace("{ARGS}", "$ARGUMENTS")
|
|
||||||
body = body.replace("{AGENT_SCRIPT}", agent_script_command)
|
|
||||||
|
|
||||||
body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name)
|
|
||||||
return CommandRegistrar.rewrite_project_relative_paths(body)
|
|
||||||
|
|
||||||
def _convert_argument_placeholder(self, content: str, from_placeholder: str, to_placeholder: str) -> str:
|
|
||||||
"""Convert argument placeholder format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
content: Command content
|
|
||||||
from_placeholder: Source placeholder (e.g., "$ARGUMENTS")
|
|
||||||
to_placeholder: Target placeholder (e.g., "{{args}}")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Content with converted placeholders
|
|
||||||
"""
|
|
||||||
return content.replace(from_placeholder, to_placeholder)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _compute_output_name(agent_name: str, cmd_name: str, agent_config: Dict[str, Any]) -> str:
|
|
||||||
"""Compute the on-disk command or skill name for an agent."""
|
|
||||||
if agent_config["extension"] != "/SKILL.md":
|
|
||||||
return cmd_name
|
|
||||||
|
|
||||||
short_name = cmd_name
|
|
||||||
if short_name.startswith("speckit."):
|
|
||||||
short_name = short_name[len("speckit."):]
|
|
||||||
short_name = short_name.replace(".", "-")
|
|
||||||
|
|
||||||
return f"speckit-{short_name}"
|
|
||||||
|
|
||||||
def register_commands(
|
|
||||||
self,
|
|
||||||
agent_name: str,
|
|
||||||
commands: List[Dict[str, Any]],
|
|
||||||
source_id: str,
|
|
||||||
source_dir: Path,
|
|
||||||
project_root: Path,
|
|
||||||
context_note: str = None
|
|
||||||
) -> List[str]:
|
|
||||||
"""Register commands for a specific agent.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
agent_name: Agent name (claude, gemini, copilot, etc.)
|
|
||||||
commands: List of command info dicts with 'name', 'file', and optional 'aliases'
|
|
||||||
source_id: Identifier of the source (extension or preset ID)
|
|
||||||
source_dir: Directory containing command source files
|
|
||||||
project_root: Path to project root
|
|
||||||
context_note: Custom context comment for markdown output
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of registered command names
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If agent is not supported
|
|
||||||
"""
|
|
||||||
self._ensure_configs()
|
|
||||||
if agent_name not in self.AGENT_CONFIGS:
|
|
||||||
raise ValueError(f"Unsupported agent: {agent_name}")
|
|
||||||
|
|
||||||
agent_config = self.AGENT_CONFIGS[agent_name]
|
|
||||||
commands_dir = project_root / agent_config["dir"]
|
|
||||||
commands_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
registered = []
|
|
||||||
|
|
||||||
for cmd_info in commands:
|
|
||||||
cmd_name = cmd_info["name"]
|
|
||||||
cmd_file = cmd_info["file"]
|
|
||||||
|
|
||||||
source_file = source_dir / cmd_file
|
|
||||||
if not source_file.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
content = source_file.read_text(encoding="utf-8")
|
|
||||||
frontmatter, body = self.parse_frontmatter(content)
|
|
||||||
|
|
||||||
frontmatter = self._adjust_script_paths(frontmatter)
|
|
||||||
|
|
||||||
body = self._convert_argument_placeholder(
|
|
||||||
body, "$ARGUMENTS", agent_config["args"]
|
|
||||||
)
|
|
||||||
|
|
||||||
output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
|
|
||||||
|
|
||||||
if agent_config["extension"] == "/SKILL.md":
|
|
||||||
output = self.render_skill_command(
|
|
||||||
agent_name, output_name, frontmatter, body, source_id, cmd_file, project_root
|
|
||||||
)
|
|
||||||
elif agent_config["format"] == "markdown":
|
|
||||||
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
|
|
||||||
elif agent_config["format"] == "toml":
|
|
||||||
output = self.render_toml_command(frontmatter, body, source_id)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported format: {agent_config['format']}")
|
|
||||||
|
|
||||||
dest_file = commands_dir / f"{output_name}{agent_config['extension']}"
|
|
||||||
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
dest_file.write_text(output, encoding="utf-8")
|
|
||||||
|
|
||||||
if agent_name == "copilot":
|
|
||||||
self.write_copilot_prompt(project_root, cmd_name)
|
|
||||||
|
|
||||||
registered.append(cmd_name)
|
|
||||||
|
|
||||||
for alias in cmd_info.get("aliases", []):
|
|
||||||
alias_output_name = self._compute_output_name(agent_name, alias, agent_config)
|
|
||||||
alias_output = output
|
|
||||||
if agent_config["extension"] == "/SKILL.md":
|
|
||||||
alias_output = self.render_skill_command(
|
|
||||||
agent_name, alias_output_name, frontmatter, body, source_id, cmd_file, project_root
|
|
||||||
)
|
|
||||||
alias_file = commands_dir / f"{alias_output_name}{agent_config['extension']}"
|
|
||||||
alias_file.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
alias_file.write_text(alias_output, encoding="utf-8")
|
|
||||||
if agent_name == "copilot":
|
|
||||||
self.write_copilot_prompt(project_root, alias)
|
|
||||||
registered.append(alias)
|
|
||||||
|
|
||||||
return registered
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_copilot_prompt(project_root: Path, cmd_name: str) -> None:
|
|
||||||
"""Generate a companion .prompt.md file for a Copilot agent command.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
project_root: Path to project root
|
|
||||||
cmd_name: Command name (e.g. 'speckit.my-ext.example')
|
|
||||||
"""
|
|
||||||
prompts_dir = project_root / ".github" / "prompts"
|
|
||||||
prompts_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
|
|
||||||
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
|
|
||||||
|
|
||||||
def register_commands_for_all_agents(
|
|
||||||
self,
|
|
||||||
commands: List[Dict[str, Any]],
|
|
||||||
source_id: str,
|
|
||||||
source_dir: Path,
|
|
||||||
project_root: Path,
|
|
||||||
context_note: str = None
|
|
||||||
) -> Dict[str, List[str]]:
|
|
||||||
"""Register commands for all detected agents in the project.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
commands: List of command info dicts
|
|
||||||
source_id: Identifier of the source (extension or preset ID)
|
|
||||||
source_dir: Directory containing command source files
|
|
||||||
project_root: Path to project root
|
|
||||||
context_note: Custom context comment for markdown output
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary mapping agent names to list of registered commands
|
|
||||||
"""
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
self._ensure_configs()
|
|
||||||
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
|
||||||
agent_dir = project_root / agent_config["dir"]
|
|
||||||
|
|
||||||
if agent_dir.exists():
|
|
||||||
try:
|
|
||||||
registered = self.register_commands(
|
|
||||||
agent_name, commands, source_id, source_dir, project_root,
|
|
||||||
context_note=context_note
|
|
||||||
)
|
|
||||||
if registered:
|
|
||||||
results[agent_name] = registered
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
def unregister_commands(
|
|
||||||
self,
|
|
||||||
registered_commands: Dict[str, List[str]],
|
|
||||||
project_root: Path
|
|
||||||
) -> None:
|
|
||||||
"""Remove previously registered command files from agent directories.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
registered_commands: Dict mapping agent names to command name lists
|
|
||||||
project_root: Path to project root
|
|
||||||
"""
|
|
||||||
self._ensure_configs()
|
|
||||||
for agent_name, cmd_names in registered_commands.items():
|
|
||||||
if agent_name not in self.AGENT_CONFIGS:
|
|
||||||
continue
|
|
||||||
|
|
||||||
agent_config = self.AGENT_CONFIGS[agent_name]
|
|
||||||
commands_dir = project_root / agent_config["dir"]
|
|
||||||
|
|
||||||
for cmd_name in cmd_names:
|
|
||||||
output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
|
|
||||||
cmd_file = commands_dir / f"{output_name}{agent_config['extension']}"
|
|
||||||
if cmd_file.exists():
|
|
||||||
cmd_file.unlink()
|
|
||||||
|
|
||||||
if agent_name == "copilot":
|
|
||||||
prompt_file = project_root / ".github" / "prompts" / f"{cmd_name}.prompt.md"
|
|
||||||
if prompt_file.exists():
|
|
||||||
prompt_file.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
# Populate AGENT_CONFIGS after class definition.
|
|
||||||
# Catches ImportError from circular imports during module loading;
|
|
||||||
# _configs_loaded stays False so the next explicit access retries.
|
|
||||||
try:
|
|
||||||
CommandRegistrar._ensure_configs()
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,105 +0,0 @@
|
|||||||
"""Integration registry for AI coding assistants.
|
|
||||||
|
|
||||||
Each integration is a self-contained subpackage that handles setup/teardown
|
|
||||||
for a specific AI assistant (Copilot, Claude, Gemini, etc.).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .base import IntegrationBase
|
|
||||||
|
|
||||||
# Maps integration key → IntegrationBase instance.
|
|
||||||
# Populated by later stages as integrations are migrated.
|
|
||||||
INTEGRATION_REGISTRY: dict[str, IntegrationBase] = {}
|
|
||||||
|
|
||||||
|
|
||||||
def _register(integration: IntegrationBase) -> None:
|
|
||||||
"""Register an integration instance in the global registry.
|
|
||||||
|
|
||||||
Raises ``ValueError`` for falsy keys and ``KeyError`` for duplicates.
|
|
||||||
"""
|
|
||||||
key = integration.key
|
|
||||||
if not key:
|
|
||||||
raise ValueError("Cannot register integration with an empty key.")
|
|
||||||
if key in INTEGRATION_REGISTRY:
|
|
||||||
raise KeyError(f"Integration with key {key!r} is already registered.")
|
|
||||||
INTEGRATION_REGISTRY[key] = integration
|
|
||||||
|
|
||||||
|
|
||||||
def get_integration(key: str) -> IntegrationBase | None:
|
|
||||||
"""Return the integration for *key*, or ``None`` if not registered."""
|
|
||||||
return INTEGRATION_REGISTRY.get(key)
|
|
||||||
|
|
||||||
|
|
||||||
# -- Register built-in integrations --------------------------------------
|
|
||||||
|
|
||||||
def _register_builtins() -> None:
|
|
||||||
"""Register all built-in integrations.
|
|
||||||
|
|
||||||
Package directories use Python-safe identifiers (e.g. ``kiro_cli``,
|
|
||||||
``cursor_agent``). The user-facing integration key stored in
|
|
||||||
``IntegrationBase.key`` stays hyphenated (``"kiro-cli"``,
|
|
||||||
``"cursor-agent"``) to match the actual CLI tool / binary name that
|
|
||||||
users install and invoke.
|
|
||||||
"""
|
|
||||||
# -- Imports (alphabetical) -------------------------------------------
|
|
||||||
from .agy import AgyIntegration
|
|
||||||
from .amp import AmpIntegration
|
|
||||||
from .auggie import AuggieIntegration
|
|
||||||
from .bob import BobIntegration
|
|
||||||
from .claude import ClaudeIntegration
|
|
||||||
from .codex import CodexIntegration
|
|
||||||
from .codebuddy import CodebuddyIntegration
|
|
||||||
from .copilot import CopilotIntegration
|
|
||||||
from .cursor_agent import CursorAgentIntegration
|
|
||||||
from .gemini import GeminiIntegration
|
|
||||||
from .generic import GenericIntegration
|
|
||||||
from .iflow import IflowIntegration
|
|
||||||
from .junie import JunieIntegration
|
|
||||||
from .kilocode import KilocodeIntegration
|
|
||||||
from .kimi import KimiIntegration
|
|
||||||
from .kiro_cli import KiroCliIntegration
|
|
||||||
from .opencode import OpencodeIntegration
|
|
||||||
from .pi import PiIntegration
|
|
||||||
from .qodercli import QodercliIntegration
|
|
||||||
from .qwen import QwenIntegration
|
|
||||||
from .roo import RooIntegration
|
|
||||||
from .shai import ShaiIntegration
|
|
||||||
from .tabnine import TabnineIntegration
|
|
||||||
from .trae import TraeIntegration
|
|
||||||
from .vibe import VibeIntegration
|
|
||||||
from .windsurf import WindsurfIntegration
|
|
||||||
|
|
||||||
# -- Registration (alphabetical) --------------------------------------
|
|
||||||
_register(AgyIntegration())
|
|
||||||
_register(AmpIntegration())
|
|
||||||
_register(AuggieIntegration())
|
|
||||||
_register(BobIntegration())
|
|
||||||
_register(ClaudeIntegration())
|
|
||||||
_register(CodexIntegration())
|
|
||||||
_register(CodebuddyIntegration())
|
|
||||||
_register(CopilotIntegration())
|
|
||||||
_register(CursorAgentIntegration())
|
|
||||||
_register(GeminiIntegration())
|
|
||||||
_register(GenericIntegration())
|
|
||||||
_register(IflowIntegration())
|
|
||||||
_register(JunieIntegration())
|
|
||||||
_register(KilocodeIntegration())
|
|
||||||
_register(KimiIntegration())
|
|
||||||
_register(KiroCliIntegration())
|
|
||||||
_register(OpencodeIntegration())
|
|
||||||
_register(PiIntegration())
|
|
||||||
_register(QodercliIntegration())
|
|
||||||
_register(QwenIntegration())
|
|
||||||
_register(RooIntegration())
|
|
||||||
_register(ShaiIntegration())
|
|
||||||
_register(TabnineIntegration())
|
|
||||||
_register(TraeIntegration())
|
|
||||||
_register(VibeIntegration())
|
|
||||||
_register(WindsurfIntegration())
|
|
||||||
|
|
||||||
|
|
||||||
_register_builtins()
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
"""Antigravity (agy) integration — skills-based agent.
|
|
||||||
|
|
||||||
Antigravity uses ``.agent/skills/speckit-<name>/SKILL.md`` layout.
|
|
||||||
Explicit command support was deprecated in version 1.20.5;
|
|
||||||
``--skills`` defaults to ``True``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ..base import IntegrationOption, SkillsIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class AgyIntegration(SkillsIntegration):
|
|
||||||
"""Integration for Antigravity IDE."""
|
|
||||||
|
|
||||||
key = "agy"
|
|
||||||
config = {
|
|
||||||
"name": "Antigravity",
|
|
||||||
"folder": ".agent/",
|
|
||||||
"commands_subdir": "skills",
|
|
||||||
"install_url": None,
|
|
||||||
"requires_cli": False,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".agent/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md",
|
|
||||||
}
|
|
||||||
context_file = "AGENTS.md"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def options(cls) -> list[IntegrationOption]:
|
|
||||||
return [
|
|
||||||
IntegrationOption(
|
|
||||||
"--skills",
|
|
||||||
is_flag=True,
|
|
||||||
default=True,
|
|
||||||
help="Install as agent skills (default for Antigravity since v1.20.5)",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# update-context.ps1 — Antigravity (agy) integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType agy
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Antigravity (agy) integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" agy
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"""Amp CLI integration."""
|
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class AmpIntegration(MarkdownIntegration):
|
|
||||||
key = "amp"
|
|
||||||
config = {
|
|
||||||
"name": "Amp",
|
|
||||||
"folder": ".agents/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": "https://ampcode.com/manual#install",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".agents/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md",
|
|
||||||
}
|
|
||||||
context_file = "AGENTS.md"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# update-context.ps1 — Amp integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Amp integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"""Auggie CLI integration."""
|
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class AuggieIntegration(MarkdownIntegration):
|
|
||||||
key = "auggie"
|
|
||||||
config = {
|
|
||||||
"name": "Auggie CLI",
|
|
||||||
"folder": ".augment/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".augment/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md",
|
|
||||||
}
|
|
||||||
context_file = ".augment/rules/specify-rules.md"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie
|
|
||||||
@@ -1,793 +0,0 @@
|
|||||||
"""Base classes for AI-assistant integrations.
|
|
||||||
|
|
||||||
Provides:
|
|
||||||
- ``IntegrationOption`` — declares a CLI option an integration accepts.
|
|
||||||
- ``IntegrationBase`` — abstract base every integration must implement.
|
|
||||||
- ``MarkdownIntegration`` — concrete base for standard Markdown-format
|
|
||||||
integrations (the common case — subclass, set three class attrs, done).
|
|
||||||
- ``TomlIntegration`` — concrete base for TOML-format integrations
|
|
||||||
(Gemini, Tabnine — subclass, set three class attrs, done).
|
|
||||||
- ``SkillsIntegration`` — concrete base for integrations that install
|
|
||||||
commands as agent skills (``speckit-<name>/SKILL.md`` layout).
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
from abc import ABC
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .manifest import IntegrationManifest
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# IntegrationOption
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class IntegrationOption:
|
|
||||||
"""Declares an option that an integration accepts via ``--integration-options``.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
name: The flag name (e.g. ``"--commands-dir"``).
|
|
||||||
is_flag: ``True`` for boolean flags (``--skills``).
|
|
||||||
required: ``True`` if the option must be supplied.
|
|
||||||
default: Default value when not supplied (``None`` → no default).
|
|
||||||
help: One-line description shown in ``specify integrate info``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
is_flag: bool = False
|
|
||||||
required: bool = False
|
|
||||||
default: Any = None
|
|
||||||
help: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# IntegrationBase — abstract base class
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class IntegrationBase(ABC):
|
|
||||||
"""Abstract base class every integration must implement.
|
|
||||||
|
|
||||||
Subclasses must set the following class-level attributes:
|
|
||||||
|
|
||||||
* ``key`` — unique identifier, matches actual CLI tool name
|
|
||||||
* ``config`` — dict compatible with ``AGENT_CONFIG`` entries
|
|
||||||
* ``registrar_config`` — dict compatible with ``CommandRegistrar.AGENT_CONFIGS``
|
|
||||||
|
|
||||||
And may optionally set:
|
|
||||||
|
|
||||||
* ``context_file`` — path (relative to project root) of the agent
|
|
||||||
context/instructions file (e.g. ``"CLAUDE.md"``)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -- Must be set by every subclass ------------------------------------
|
|
||||||
|
|
||||||
key: str = ""
|
|
||||||
"""Unique integration key — should match the actual CLI tool name."""
|
|
||||||
|
|
||||||
config: dict[str, Any] | None = None
|
|
||||||
"""Metadata dict matching the ``AGENT_CONFIG`` shape."""
|
|
||||||
|
|
||||||
registrar_config: dict[str, Any] | None = None
|
|
||||||
"""Registration dict matching ``CommandRegistrar.AGENT_CONFIGS`` shape."""
|
|
||||||
|
|
||||||
# -- Optional ---------------------------------------------------------
|
|
||||||
|
|
||||||
context_file: str | None = None
|
|
||||||
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
|
|
||||||
|
|
||||||
# -- Public API -------------------------------------------------------
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def options(cls) -> list[IntegrationOption]:
|
|
||||||
"""Return options this integration accepts. Default: none."""
|
|
||||||
return []
|
|
||||||
|
|
||||||
# -- Primitives — building blocks for setup() -------------------------
|
|
||||||
|
|
||||||
def shared_commands_dir(self) -> Path | None:
|
|
||||||
"""Return path to the shared command templates directory.
|
|
||||||
|
|
||||||
Checks ``core_pack/commands/`` (wheel install) first, then
|
|
||||||
``templates/commands/`` (source checkout). Returns ``None``
|
|
||||||
if neither exists.
|
|
||||||
"""
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
pkg_dir = Path(inspect.getfile(IntegrationBase)).resolve().parent.parent
|
|
||||||
for candidate in [
|
|
||||||
pkg_dir / "core_pack" / "commands",
|
|
||||||
pkg_dir.parent.parent / "templates" / "commands",
|
|
||||||
]:
|
|
||||||
if candidate.is_dir():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
def shared_templates_dir(self) -> Path | None:
|
|
||||||
"""Return path to the shared page templates directory.
|
|
||||||
|
|
||||||
Contains ``vscode-settings.json``, ``spec-template.md``, etc.
|
|
||||||
Checks ``core_pack/templates/`` then ``templates/``.
|
|
||||||
"""
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
pkg_dir = Path(inspect.getfile(IntegrationBase)).resolve().parent.parent
|
|
||||||
for candidate in [
|
|
||||||
pkg_dir / "core_pack" / "templates",
|
|
||||||
pkg_dir.parent.parent / "templates",
|
|
||||||
]:
|
|
||||||
if candidate.is_dir():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
def list_command_templates(self) -> list[Path]:
|
|
||||||
"""Return sorted list of command template files from the shared directory."""
|
|
||||||
cmd_dir = self.shared_commands_dir()
|
|
||||||
if not cmd_dir or not cmd_dir.is_dir():
|
|
||||||
return []
|
|
||||||
return sorted(f for f in cmd_dir.iterdir() if f.is_file() and f.suffix == ".md")
|
|
||||||
|
|
||||||
def command_filename(self, template_name: str) -> str:
|
|
||||||
"""Return the destination filename for a command template.
|
|
||||||
|
|
||||||
*template_name* is the stem of the source file (e.g. ``"plan"``).
|
|
||||||
Default: ``speckit.{template_name}.md``. Subclasses override
|
|
||||||
to change the extension or naming convention.
|
|
||||||
"""
|
|
||||||
return f"speckit.{template_name}.md"
|
|
||||||
|
|
||||||
def commands_dest(self, project_root: Path) -> Path:
|
|
||||||
"""Return the absolute path to the commands output directory.
|
|
||||||
|
|
||||||
Derived from ``config["folder"]`` and ``config["commands_subdir"]``.
|
|
||||||
Raises ``ValueError`` if ``config`` or ``folder`` is missing.
|
|
||||||
"""
|
|
||||||
if not self.config:
|
|
||||||
raise ValueError(
|
|
||||||
f"{type(self).__name__}.config is not set; integration "
|
|
||||||
"subclasses must define a non-empty 'config' mapping."
|
|
||||||
)
|
|
||||||
folder = self.config.get("folder")
|
|
||||||
if not folder:
|
|
||||||
raise ValueError(
|
|
||||||
f"{type(self).__name__}.config is missing required 'folder' entry."
|
|
||||||
)
|
|
||||||
subdir = self.config.get("commands_subdir", "commands")
|
|
||||||
return project_root / folder / subdir
|
|
||||||
|
|
||||||
# -- File operations — granular primitives for setup() ----------------
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def copy_command_to_directory(
|
|
||||||
src: Path,
|
|
||||||
dest_dir: Path,
|
|
||||||
filename: str,
|
|
||||||
) -> Path:
|
|
||||||
"""Copy a command template to *dest_dir* with the given *filename*.
|
|
||||||
|
|
||||||
Creates *dest_dir* if needed. Returns the absolute path of the
|
|
||||||
written file. The caller can post-process the file before
|
|
||||||
recording it in the manifest.
|
|
||||||
"""
|
|
||||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
dst = dest_dir / filename
|
|
||||||
shutil.copy2(src, dst)
|
|
||||||
return dst
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def record_file_in_manifest(
|
|
||||||
file_path: Path,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
) -> None:
|
|
||||||
"""Hash *file_path* and record it in *manifest*.
|
|
||||||
|
|
||||||
*file_path* must be inside *project_root*.
|
|
||||||
"""
|
|
||||||
rel = file_path.resolve().relative_to(project_root.resolve())
|
|
||||||
manifest.record_existing(rel)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def write_file_and_record(
|
|
||||||
content: str,
|
|
||||||
dest: Path,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
) -> Path:
|
|
||||||
"""Write *content* to *dest*, hash it, and record in *manifest*.
|
|
||||||
|
|
||||||
Creates parent directories as needed. Writes bytes directly to
|
|
||||||
avoid platform newline translation (CRLF on Windows). Any
|
|
||||||
``\r\n`` sequences in *content* are normalised to ``\n`` before
|
|
||||||
writing. Returns *dest*.
|
|
||||||
"""
|
|
||||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
normalized = content.replace("\r\n", "\n")
|
|
||||||
dest.write_bytes(normalized.encode("utf-8"))
|
|
||||||
rel = dest.resolve().relative_to(project_root.resolve())
|
|
||||||
manifest.record_existing(rel)
|
|
||||||
return dest
|
|
||||||
|
|
||||||
def integration_scripts_dir(self) -> Path | None:
|
|
||||||
"""Return path to this integration's bundled ``scripts/`` directory.
|
|
||||||
|
|
||||||
Looks for a ``scripts/`` sibling of the module that defines the
|
|
||||||
concrete subclass (not ``IntegrationBase`` itself).
|
|
||||||
Returns ``None`` if the directory doesn't exist.
|
|
||||||
"""
|
|
||||||
import inspect
|
|
||||||
|
|
||||||
cls_file = inspect.getfile(type(self))
|
|
||||||
scripts = Path(cls_file).resolve().parent / "scripts"
|
|
||||||
return scripts if scripts.is_dir() else None
|
|
||||||
|
|
||||||
def install_scripts(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""Copy integration-specific scripts into the project.
|
|
||||||
|
|
||||||
Copies files from this integration's ``scripts/`` directory to
|
|
||||||
``.specify/integrations/<key>/scripts/`` in the project. Shell
|
|
||||||
scripts are made executable. All copied files are recorded in
|
|
||||||
*manifest*.
|
|
||||||
|
|
||||||
Returns the list of files created.
|
|
||||||
"""
|
|
||||||
scripts_src = self.integration_scripts_dir()
|
|
||||||
if not scripts_src:
|
|
||||||
return []
|
|
||||||
|
|
||||||
created: list[Path] = []
|
|
||||||
scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts"
|
|
||||||
scripts_dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
for src_script in sorted(scripts_src.iterdir()):
|
|
||||||
if not src_script.is_file():
|
|
||||||
continue
|
|
||||||
dst_script = scripts_dest / src_script.name
|
|
||||||
shutil.copy2(src_script, dst_script)
|
|
||||||
if dst_script.suffix == ".sh":
|
|
||||||
dst_script.chmod(dst_script.stat().st_mode | 0o111)
|
|
||||||
self.record_file_in_manifest(dst_script, project_root, manifest)
|
|
||||||
created.append(dst_script)
|
|
||||||
|
|
||||||
return created
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def process_template(
|
|
||||||
content: str,
|
|
||||||
agent_name: str,
|
|
||||||
script_type: str,
|
|
||||||
arg_placeholder: str = "$ARGUMENTS",
|
|
||||||
) -> str:
|
|
||||||
"""Process a raw command template into agent-ready content.
|
|
||||||
|
|
||||||
Performs the same transformations as the release script:
|
|
||||||
1. Extract ``scripts.<script_type>`` value from YAML frontmatter
|
|
||||||
2. Replace ``{SCRIPT}`` with the extracted script command
|
|
||||||
3. Extract ``agent_scripts.<script_type>`` and replace ``{AGENT_SCRIPT}``
|
|
||||||
4. Strip ``scripts:`` and ``agent_scripts:`` sections from frontmatter
|
|
||||||
5. Replace ``{ARGS}`` with *arg_placeholder*
|
|
||||||
6. Replace ``__AGENT__`` with *agent_name*
|
|
||||||
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
|
||||||
"""
|
|
||||||
# 1. Extract script command from frontmatter
|
|
||||||
script_command = ""
|
|
||||||
script_pattern = re.compile(
|
|
||||||
rf"^\s*{re.escape(script_type)}:\s*(.+)$", re.MULTILINE
|
|
||||||
)
|
|
||||||
# Find the scripts: block
|
|
||||||
in_scripts = False
|
|
||||||
for line in content.splitlines():
|
|
||||||
if line.strip() == "scripts:":
|
|
||||||
in_scripts = True
|
|
||||||
continue
|
|
||||||
if in_scripts and line and not line[0].isspace():
|
|
||||||
in_scripts = False
|
|
||||||
if in_scripts:
|
|
||||||
m = script_pattern.match(line)
|
|
||||||
if m:
|
|
||||||
script_command = m.group(1).strip()
|
|
||||||
break
|
|
||||||
|
|
||||||
# 2. Replace {SCRIPT}
|
|
||||||
if script_command:
|
|
||||||
content = content.replace("{SCRIPT}", script_command)
|
|
||||||
|
|
||||||
# 3. Extract agent_script command
|
|
||||||
agent_script_command = ""
|
|
||||||
in_agent_scripts = False
|
|
||||||
for line in content.splitlines():
|
|
||||||
if line.strip() == "agent_scripts:":
|
|
||||||
in_agent_scripts = True
|
|
||||||
continue
|
|
||||||
if in_agent_scripts and line and not line[0].isspace():
|
|
||||||
in_agent_scripts = False
|
|
||||||
if in_agent_scripts:
|
|
||||||
m = script_pattern.match(line)
|
|
||||||
if m:
|
|
||||||
agent_script_command = m.group(1).strip()
|
|
||||||
break
|
|
||||||
|
|
||||||
if agent_script_command:
|
|
||||||
content = content.replace("{AGENT_SCRIPT}", agent_script_command)
|
|
||||||
|
|
||||||
# 4. Strip scripts: and agent_scripts: sections from frontmatter
|
|
||||||
lines = content.splitlines(keepends=True)
|
|
||||||
output_lines: list[str] = []
|
|
||||||
in_frontmatter = False
|
|
||||||
skip_section = False
|
|
||||||
dash_count = 0
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.rstrip("\n\r")
|
|
||||||
if stripped == "---":
|
|
||||||
dash_count += 1
|
|
||||||
if dash_count == 1:
|
|
||||||
in_frontmatter = True
|
|
||||||
else:
|
|
||||||
in_frontmatter = False
|
|
||||||
skip_section = False
|
|
||||||
output_lines.append(line)
|
|
||||||
continue
|
|
||||||
if in_frontmatter:
|
|
||||||
if stripped in ("scripts:", "agent_scripts:"):
|
|
||||||
skip_section = True
|
|
||||||
continue
|
|
||||||
if skip_section:
|
|
||||||
if line[0:1].isspace():
|
|
||||||
continue # skip indented content under scripts/agent_scripts
|
|
||||||
skip_section = False
|
|
||||||
output_lines.append(line)
|
|
||||||
content = "".join(output_lines)
|
|
||||||
|
|
||||||
# 5. Replace {ARGS}
|
|
||||||
content = content.replace("{ARGS}", arg_placeholder)
|
|
||||||
|
|
||||||
# 6. Replace __AGENT__
|
|
||||||
content = content.replace("__AGENT__", agent_name)
|
|
||||||
|
|
||||||
# 7. Rewrite paths — delegate to the shared implementation in
|
|
||||||
# CommandRegistrar so extension-local paths are preserved and
|
|
||||||
# boundary rules stay consistent across the codebase.
|
|
||||||
from specify_cli.agents import CommandRegistrar
|
|
||||||
content = CommandRegistrar.rewrite_project_relative_paths(content)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""Install integration command files into *project_root*.
|
|
||||||
|
|
||||||
Returns the list of files created. Copies raw templates without
|
|
||||||
processing. Integrations that need placeholder replacement
|
|
||||||
(e.g. ``{SCRIPT}``, ``__AGENT__``) should override ``setup()``
|
|
||||||
and call ``process_template()`` in their own loop — see
|
|
||||||
``CopilotIntegration`` for an example.
|
|
||||||
"""
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = self.commands_dest(project_root).resolve()
|
|
||||||
try:
|
|
||||||
dest.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Integration destination {dest} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
for src_file in templates:
|
|
||||||
dst_name = self.command_filename(src_file.stem)
|
|
||||||
dst_file = self.copy_command_to_directory(src_file, dest, dst_name)
|
|
||||||
self.record_file_in_manifest(dst_file, project_root, manifest)
|
|
||||||
created.append(dst_file)
|
|
||||||
|
|
||||||
return created
|
|
||||||
|
|
||||||
def teardown(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
*,
|
|
||||||
force: bool = False,
|
|
||||||
) -> tuple[list[Path], list[Path]]:
|
|
||||||
"""Uninstall integration files from *project_root*.
|
|
||||||
|
|
||||||
Delegates to ``manifest.uninstall()`` which only removes files
|
|
||||||
whose hash still matches the recorded value (unless *force*).
|
|
||||||
|
|
||||||
Returns ``(removed, skipped)`` file lists.
|
|
||||||
"""
|
|
||||||
return manifest.uninstall(project_root, force=force)
|
|
||||||
|
|
||||||
# -- Convenience helpers for subclasses -------------------------------
|
|
||||||
|
|
||||||
def install(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""High-level install — calls ``setup()`` and returns created files."""
|
|
||||||
return self.setup(
|
|
||||||
project_root, manifest, parsed_options=parsed_options, **opts
|
|
||||||
)
|
|
||||||
|
|
||||||
def uninstall(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
*,
|
|
||||||
force: bool = False,
|
|
||||||
) -> tuple[list[Path], list[Path]]:
|
|
||||||
"""High-level uninstall — calls ``teardown()``."""
|
|
||||||
return self.teardown(project_root, manifest, force=force)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# MarkdownIntegration — covers ~20 standard agents
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class MarkdownIntegration(IntegrationBase):
|
|
||||||
"""Concrete base for integrations that use standard Markdown commands.
|
|
||||||
|
|
||||||
Subclasses only need to set ``key``, ``config``, ``registrar_config``
|
|
||||||
(and optionally ``context_file``). Everything else is inherited.
|
|
||||||
|
|
||||||
``setup()`` processes command templates (replacing ``{SCRIPT}``,
|
|
||||||
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
|
|
||||||
integration-specific scripts (``update-context.sh`` / ``.ps1``).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = self.commands_dest(project_root).resolve()
|
|
||||||
try:
|
|
||||||
dest.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Integration destination {dest} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
script_type = opts.get("script_type", "sh")
|
|
||||||
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS"
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
for src_file in templates:
|
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
|
||||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
|
||||||
dst_name = self.command_filename(src_file.stem)
|
|
||||||
dst_file = self.write_file_and_record(
|
|
||||||
processed, dest / dst_name, project_root, manifest
|
|
||||||
)
|
|
||||||
created.append(dst_file)
|
|
||||||
|
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
|
||||||
return created
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# TomlIntegration — TOML-format agents (Gemini, Tabnine)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
class TomlIntegration(IntegrationBase):
|
|
||||||
"""Concrete base for integrations that use TOML command format.
|
|
||||||
|
|
||||||
Mirrors ``MarkdownIntegration`` closely: subclasses only need to set
|
|
||||||
``key``, ``config``, ``registrar_config`` (and optionally
|
|
||||||
``context_file``). Everything else is inherited.
|
|
||||||
|
|
||||||
``setup()`` processes command templates through the same placeholder
|
|
||||||
pipeline as ``MarkdownIntegration``, then converts the result to
|
|
||||||
TOML format (``description`` key + ``prompt`` multiline string).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def command_filename(self, template_name: str) -> str:
|
|
||||||
"""TOML commands use ``.toml`` extension."""
|
|
||||||
return f"speckit.{template_name}.toml"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _extract_description(content: str) -> str:
|
|
||||||
"""Extract the ``description`` value from YAML frontmatter.
|
|
||||||
|
|
||||||
Scans lines between the first pair of ``---`` delimiters for a
|
|
||||||
top-level ``description:`` key. Returns the value (with
|
|
||||||
surrounding quotes stripped) or an empty string if not found.
|
|
||||||
"""
|
|
||||||
in_frontmatter = False
|
|
||||||
for line in content.splitlines():
|
|
||||||
stripped = line.rstrip("\n\r")
|
|
||||||
if stripped == "---":
|
|
||||||
if not in_frontmatter:
|
|
||||||
in_frontmatter = True
|
|
||||||
continue
|
|
||||||
break # second ---
|
|
||||||
if in_frontmatter and stripped.startswith("description:"):
|
|
||||||
_, _, value = stripped.partition(":")
|
|
||||||
return value.strip().strip('"').strip("'")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _render_toml(description: str, body: str) -> str:
|
|
||||||
"""Render a TOML command file from description and body.
|
|
||||||
|
|
||||||
Uses multiline basic strings (``\"\"\"``) with backslashes
|
|
||||||
escaped, matching the output of the release script. Falls back
|
|
||||||
to multiline literal strings (``'''``) if the body contains
|
|
||||||
``\"\"\"``, then to an escaped basic string as a last resort.
|
|
||||||
|
|
||||||
The body is rstrip'd so the closing delimiter appears on the line
|
|
||||||
immediately after the last content line — matching the release
|
|
||||||
script's ``echo "$body"; echo '\"\"\"'`` pattern.
|
|
||||||
"""
|
|
||||||
toml_lines: list[str] = []
|
|
||||||
|
|
||||||
if description:
|
|
||||||
desc = description.replace('"', '\\"')
|
|
||||||
toml_lines.append(f'description = "{desc}"')
|
|
||||||
toml_lines.append("")
|
|
||||||
|
|
||||||
body = body.rstrip("\n")
|
|
||||||
|
|
||||||
# Escape backslashes for basic multiline strings.
|
|
||||||
escaped = body.replace("\\", "\\\\")
|
|
||||||
|
|
||||||
if '"""' not in escaped:
|
|
||||||
toml_lines.append('prompt = """')
|
|
||||||
toml_lines.append(escaped)
|
|
||||||
toml_lines.append('"""')
|
|
||||||
elif "'''" not in body:
|
|
||||||
toml_lines.append("prompt = '''")
|
|
||||||
toml_lines.append(body)
|
|
||||||
toml_lines.append("'''")
|
|
||||||
else:
|
|
||||||
escaped_body = (
|
|
||||||
body.replace("\\", "\\\\")
|
|
||||||
.replace('"', '\\"')
|
|
||||||
.replace("\n", "\\n")
|
|
||||||
.replace("\r", "\\r")
|
|
||||||
.replace("\t", "\\t")
|
|
||||||
)
|
|
||||||
toml_lines.append(f'prompt = "{escaped_body}"')
|
|
||||||
|
|
||||||
return "\n".join(toml_lines) + "\n"
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = self.commands_dest(project_root).resolve()
|
|
||||||
try:
|
|
||||||
dest.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Integration destination {dest} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
script_type = opts.get("script_type", "sh")
|
|
||||||
arg_placeholder = self.registrar_config.get("args", "{{args}}") if self.registrar_config else "{{args}}"
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
for src_file in templates:
|
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
|
||||||
description = self._extract_description(raw)
|
|
||||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
|
||||||
toml_content = self._render_toml(description, processed)
|
|
||||||
dst_name = self.command_filename(src_file.stem)
|
|
||||||
dst_file = self.write_file_and_record(
|
|
||||||
toml_content, dest / dst_name, project_root, manifest
|
|
||||||
)
|
|
||||||
created.append(dst_file)
|
|
||||||
|
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
|
||||||
return created
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# SkillsIntegration — skills-format agents (Codex, Kimi, Agy)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class SkillsIntegration(IntegrationBase):
|
|
||||||
"""Concrete base for integrations that install commands as agent skills.
|
|
||||||
|
|
||||||
Skills use the ``speckit-<name>/SKILL.md`` directory layout following
|
|
||||||
the `agentskills.io <https://agentskills.io/specification>`_ spec.
|
|
||||||
|
|
||||||
Subclasses set ``key``, ``config``, ``registrar_config`` (and
|
|
||||||
optionally ``context_file``) like any integration. They may also
|
|
||||||
override ``options()`` to declare additional CLI flags (e.g.
|
|
||||||
``--skills``, ``--migrate-legacy``).
|
|
||||||
|
|
||||||
``setup()`` processes each shared command template into a
|
|
||||||
``speckit-<name>/SKILL.md`` file with skills-oriented frontmatter.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def skills_dest(self, project_root: Path) -> Path:
|
|
||||||
"""Return the absolute path to the skills output directory.
|
|
||||||
|
|
||||||
Derived from ``config["folder"]`` and the configured
|
|
||||||
``commands_subdir`` (defaults to ``"skills"``).
|
|
||||||
|
|
||||||
Raises ``ValueError`` when ``config`` or ``folder`` is missing.
|
|
||||||
"""
|
|
||||||
if not self.config:
|
|
||||||
raise ValueError(
|
|
||||||
f"{type(self).__name__}.config is not set."
|
|
||||||
)
|
|
||||||
folder = self.config.get("folder")
|
|
||||||
if not folder:
|
|
||||||
raise ValueError(
|
|
||||||
f"{type(self).__name__}.config is missing required 'folder' entry."
|
|
||||||
)
|
|
||||||
subdir = self.config.get("commands_subdir", "skills")
|
|
||||||
return project_root / folder / subdir
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""Install command templates as agent skills.
|
|
||||||
|
|
||||||
Creates ``speckit-<name>/SKILL.md`` for each shared command
|
|
||||||
template. Each SKILL.md has normalised frontmatter containing
|
|
||||||
``name``, ``description``, ``compatibility``, and ``metadata``.
|
|
||||||
"""
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
skills_dir = self.skills_dest(project_root).resolve()
|
|
||||||
try:
|
|
||||||
skills_dir.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Skills destination {skills_dir} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
script_type = opts.get("script_type", "sh")
|
|
||||||
arg_placeholder = (
|
|
||||||
self.registrar_config.get("args", "$ARGUMENTS")
|
|
||||||
if self.registrar_config
|
|
||||||
else "$ARGUMENTS"
|
|
||||||
)
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
for src_file in templates:
|
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
# Derive the skill name from the template stem
|
|
||||||
command_name = src_file.stem # e.g. "plan"
|
|
||||||
skill_name = f"speckit-{command_name.replace('.', '-')}"
|
|
||||||
|
|
||||||
# Parse frontmatter for description
|
|
||||||
frontmatter: dict[str, Any] = {}
|
|
||||||
if raw.startswith("---"):
|
|
||||||
parts = raw.split("---", 2)
|
|
||||||
if len(parts) >= 3:
|
|
||||||
try:
|
|
||||||
fm = yaml.safe_load(parts[1])
|
|
||||||
if isinstance(fm, dict):
|
|
||||||
frontmatter = fm
|
|
||||||
except yaml.YAMLError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Process body through the standard template pipeline
|
|
||||||
processed_body = self.process_template(
|
|
||||||
raw, self.key, script_type, arg_placeholder
|
|
||||||
)
|
|
||||||
# Strip the processed frontmatter — we rebuild it for skills.
|
|
||||||
# Preserve leading whitespace in the body to match release ZIP
|
|
||||||
# output byte-for-byte (the template body starts with \n after
|
|
||||||
# the closing ---).
|
|
||||||
if processed_body.startswith("---"):
|
|
||||||
parts = processed_body.split("---", 2)
|
|
||||||
if len(parts) >= 3:
|
|
||||||
processed_body = parts[2]
|
|
||||||
|
|
||||||
# Select description — use the original template description
|
|
||||||
# to stay byte-for-byte identical with release ZIP output.
|
|
||||||
description = frontmatter.get("description", "")
|
|
||||||
if not description:
|
|
||||||
description = f"Spec Kit: {command_name} workflow"
|
|
||||||
|
|
||||||
# Build SKILL.md with manually formatted frontmatter to match
|
|
||||||
# the release packaging script output exactly (double-quoted
|
|
||||||
# values, no yaml.safe_dump quoting differences).
|
|
||||||
def _quote(v: str) -> str:
|
|
||||||
escaped = v.replace("\\", "\\\\").replace('"', '\\"')
|
|
||||||
return f'"{escaped}"'
|
|
||||||
|
|
||||||
skill_content = (
|
|
||||||
f"---\n"
|
|
||||||
f"name: {_quote(skill_name)}\n"
|
|
||||||
f"description: {_quote(description)}\n"
|
|
||||||
f"compatibility: {_quote('Requires spec-kit project structure with .specify/ directory')}\n"
|
|
||||||
f"metadata:\n"
|
|
||||||
f" author: {_quote('github-spec-kit')}\n"
|
|
||||||
f" source: {_quote('templates/commands/' + src_file.name)}\n"
|
|
||||||
f"---\n"
|
|
||||||
f"{processed_body}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Write speckit-<name>/SKILL.md
|
|
||||||
skill_dir = skills_dir / skill_name
|
|
||||||
skill_file = skill_dir / "SKILL.md"
|
|
||||||
dst = self.write_file_and_record(
|
|
||||||
skill_content, skill_file, project_root, manifest
|
|
||||||
)
|
|
||||||
created.append(dst)
|
|
||||||
|
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
|
||||||
return created
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"""IBM Bob integration."""
|
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class BobIntegration(MarkdownIntegration):
|
|
||||||
key = "bob"
|
|
||||||
config = {
|
|
||||||
"name": "IBM Bob",
|
|
||||||
"folder": ".bob/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": None,
|
|
||||||
"requires_cli": False,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".bob/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md",
|
|
||||||
}
|
|
||||||
context_file = "AGENTS.md"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# update-context.ps1 — IBM Bob integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType bob
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — IBM Bob integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" bob
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""Claude Code integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from ..base import SkillsIntegration
|
|
||||||
from ..manifest import IntegrationManifest
|
|
||||||
|
|
||||||
|
|
||||||
class ClaudeIntegration(SkillsIntegration):
|
|
||||||
"""Integration for Claude Code skills."""
|
|
||||||
|
|
||||||
key = "claude"
|
|
||||||
config = {
|
|
||||||
"name": "Claude Code",
|
|
||||||
"folder": ".claude/",
|
|
||||||
"commands_subdir": "skills",
|
|
||||||
"install_url": "https://docs.anthropic.com/en/docs/claude-code/setup",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".claude/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md",
|
|
||||||
}
|
|
||||||
context_file = "CLAUDE.md"
|
|
||||||
|
|
||||||
def command_filename(self, template_name: str) -> str:
|
|
||||||
"""Claude skills live at .claude/skills/<name>/SKILL.md."""
|
|
||||||
skill_name = f"speckit-{template_name.replace('.', '-')}"
|
|
||||||
return f"{skill_name}/SKILL.md"
|
|
||||||
|
|
||||||
def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
|
|
||||||
"""Render a processed command template as a Claude skill."""
|
|
||||||
skill_name = f"speckit-{template_name.replace('.', '-')}"
|
|
||||||
description = frontmatter.get(
|
|
||||||
"description",
|
|
||||||
f"Spec-kit workflow command: {template_name}",
|
|
||||||
)
|
|
||||||
skill_frontmatter = self._build_skill_fm(
|
|
||||||
skill_name, description, f"templates/commands/{template_name}.md"
|
|
||||||
)
|
|
||||||
frontmatter_text = yaml.safe_dump(skill_frontmatter, sort_keys=False).strip()
|
|
||||||
return f"---\n{frontmatter_text}\n---\n\n{body.strip()}\n"
|
|
||||||
|
|
||||||
def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
|
|
||||||
from specify_cli.agents import CommandRegistrar
|
|
||||||
return CommandRegistrar.build_skill_frontmatter(
|
|
||||||
self.key, name, description, source
|
|
||||||
)
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""Install Claude skills into .claude/skills."""
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
dest = self.skills_dest(project_root).resolve()
|
|
||||||
try:
|
|
||||||
dest.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Integration destination {dest} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
script_type = opts.get("script_type", "sh")
|
|
||||||
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
|
|
||||||
from specify_cli.agents import CommandRegistrar
|
|
||||||
registrar = CommandRegistrar()
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
for src_file in templates:
|
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
|
||||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
|
||||||
frontmatter, body = registrar.parse_frontmatter(processed)
|
|
||||||
if not isinstance(frontmatter, dict):
|
|
||||||
frontmatter = {}
|
|
||||||
|
|
||||||
rendered = self._render_skill(src_file.stem, frontmatter, body)
|
|
||||||
dst_file = self.write_file_and_record(
|
|
||||||
rendered,
|
|
||||||
dest / self.command_filename(src_file.stem),
|
|
||||||
project_root,
|
|
||||||
manifest,
|
|
||||||
)
|
|
||||||
created.append(dst_file)
|
|
||||||
|
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
|
||||||
return created
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# update-context.ps1 — Claude Code integration: create/update CLAUDE.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType claude
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Claude Code integration: create/update CLAUDE.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" claude
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"""CodeBuddy CLI integration."""
|
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class CodebuddyIntegration(MarkdownIntegration):
|
|
||||||
key = "codebuddy"
|
|
||||||
config = {
|
|
||||||
"name": "CodeBuddy",
|
|
||||||
"folder": ".codebuddy/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": "https://www.codebuddy.ai/cli",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".codebuddy/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md",
|
|
||||||
}
|
|
||||||
context_file = "CODEBUDDY.md"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# update-context.ps1 — CodeBuddy integration: create/update CODEBUDDY.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codebuddy
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — CodeBuddy integration: create/update CODEBUDDY.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codebuddy
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
"""Codex CLI integration — skills-based agent.
|
|
||||||
|
|
||||||
Codex uses the ``.agents/skills/speckit-<name>/SKILL.md`` layout.
|
|
||||||
Commands are deprecated; ``--skills`` defaults to ``True``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from ..base import IntegrationOption, SkillsIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class CodexIntegration(SkillsIntegration):
|
|
||||||
"""Integration for OpenAI Codex CLI."""
|
|
||||||
|
|
||||||
key = "codex"
|
|
||||||
config = {
|
|
||||||
"name": "Codex CLI",
|
|
||||||
"folder": ".agents/",
|
|
||||||
"commands_subdir": "skills",
|
|
||||||
"install_url": "https://github.com/openai/codex",
|
|
||||||
"requires_cli": True,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".agents/skills",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": "/SKILL.md",
|
|
||||||
}
|
|
||||||
context_file = "AGENTS.md"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def options(cls) -> list[IntegrationOption]:
|
|
||||||
return [
|
|
||||||
IntegrationOption(
|
|
||||||
"--skills",
|
|
||||||
is_flag=True,
|
|
||||||
default=True,
|
|
||||||
help="Install as agent skills (default for Codex)",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# update-context.ps1 — Codex CLI integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType codex
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Codex CLI integration: create/update AGENTS.md
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" codex
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
"""Copilot integration — GitHub Copilot in VS Code.
|
|
||||||
|
|
||||||
Copilot has several unique behaviors compared to standard markdown agents:
|
|
||||||
- Commands use ``.agent.md`` extension (not ``.md``)
|
|
||||||
- Each command gets a companion ``.prompt.md`` file in ``.github/prompts/``
|
|
||||||
- Installs ``.vscode/settings.json`` with prompt file recommendations
|
|
||||||
- Context file lives at ``.github/copilot-instructions.md``
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
import shutil
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ..base import IntegrationBase
|
|
||||||
from ..manifest import IntegrationManifest
|
|
||||||
|
|
||||||
|
|
||||||
class CopilotIntegration(IntegrationBase):
|
|
||||||
"""Integration for GitHub Copilot in VS Code."""
|
|
||||||
|
|
||||||
key = "copilot"
|
|
||||||
config = {
|
|
||||||
"name": "GitHub Copilot",
|
|
||||||
"folder": ".github/",
|
|
||||||
"commands_subdir": "agents",
|
|
||||||
"install_url": None,
|
|
||||||
"requires_cli": False,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".github/agents",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".agent.md",
|
|
||||||
}
|
|
||||||
context_file = ".github/copilot-instructions.md"
|
|
||||||
|
|
||||||
def command_filename(self, template_name: str) -> str:
|
|
||||||
"""Copilot commands use ``.agent.md`` extension."""
|
|
||||||
return f"speckit.{template_name}.agent.md"
|
|
||||||
|
|
||||||
def setup(
|
|
||||||
self,
|
|
||||||
project_root: Path,
|
|
||||||
manifest: IntegrationManifest,
|
|
||||||
parsed_options: dict[str, Any] | None = None,
|
|
||||||
**opts: Any,
|
|
||||||
) -> list[Path]:
|
|
||||||
"""Install copilot commands, companion prompts, and VS Code settings.
|
|
||||||
|
|
||||||
Uses base class primitives to: read templates, process them
|
|
||||||
(replace placeholders, strip script blocks, rewrite paths),
|
|
||||||
write as ``.agent.md``, then add companion prompts and VS Code settings.
|
|
||||||
"""
|
|
||||||
project_root_resolved = project_root.resolve()
|
|
||||||
if manifest.project_root != project_root_resolved:
|
|
||||||
raise ValueError(
|
|
||||||
f"manifest.project_root ({manifest.project_root}) does not match "
|
|
||||||
f"project_root ({project_root_resolved})"
|
|
||||||
)
|
|
||||||
|
|
||||||
templates = self.list_command_templates()
|
|
||||||
if not templates:
|
|
||||||
return []
|
|
||||||
|
|
||||||
dest = self.commands_dest(project_root)
|
|
||||||
dest_resolved = dest.resolve()
|
|
||||||
try:
|
|
||||||
dest_resolved.relative_to(project_root_resolved)
|
|
||||||
except ValueError as exc:
|
|
||||||
raise ValueError(
|
|
||||||
f"Integration destination {dest_resolved} escapes "
|
|
||||||
f"project root {project_root_resolved}"
|
|
||||||
) from exc
|
|
||||||
dest.mkdir(parents=True, exist_ok=True)
|
|
||||||
created: list[Path] = []
|
|
||||||
|
|
||||||
script_type = opts.get("script_type", "sh")
|
|
||||||
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
|
|
||||||
|
|
||||||
# 1. Process and write command files as .agent.md
|
|
||||||
for src_file in templates:
|
|
||||||
raw = src_file.read_text(encoding="utf-8")
|
|
||||||
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
|
|
||||||
dst_name = self.command_filename(src_file.stem)
|
|
||||||
dst_file = self.write_file_and_record(
|
|
||||||
processed, dest / dst_name, project_root, manifest
|
|
||||||
)
|
|
||||||
created.append(dst_file)
|
|
||||||
|
|
||||||
# 2. Generate companion .prompt.md files from the templates we just wrote
|
|
||||||
prompts_dir = project_root / ".github" / "prompts"
|
|
||||||
for src_file in templates:
|
|
||||||
cmd_name = f"speckit.{src_file.stem}"
|
|
||||||
prompt_content = f"---\nagent: {cmd_name}\n---\n"
|
|
||||||
prompt_file = self.write_file_and_record(
|
|
||||||
prompt_content,
|
|
||||||
prompts_dir / f"{cmd_name}.prompt.md",
|
|
||||||
project_root,
|
|
||||||
manifest,
|
|
||||||
)
|
|
||||||
created.append(prompt_file)
|
|
||||||
|
|
||||||
# Write .vscode/settings.json
|
|
||||||
settings_src = self._vscode_settings_path()
|
|
||||||
if settings_src and settings_src.is_file():
|
|
||||||
dst_settings = project_root / ".vscode" / "settings.json"
|
|
||||||
dst_settings.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
if dst_settings.exists():
|
|
||||||
# Merge into existing — don't track since we can't safely
|
|
||||||
# remove the user's settings file on uninstall.
|
|
||||||
self._merge_vscode_settings(settings_src, dst_settings)
|
|
||||||
else:
|
|
||||||
shutil.copy2(settings_src, dst_settings)
|
|
||||||
self.record_file_in_manifest(dst_settings, project_root, manifest)
|
|
||||||
created.append(dst_settings)
|
|
||||||
|
|
||||||
# 4. Install integration-specific update-context scripts
|
|
||||||
created.extend(self.install_scripts(project_root, manifest))
|
|
||||||
|
|
||||||
return created
|
|
||||||
|
|
||||||
def _vscode_settings_path(self) -> Path | None:
|
|
||||||
"""Return path to the bundled vscode-settings.json template."""
|
|
||||||
tpl_dir = self.shared_templates_dir()
|
|
||||||
if tpl_dir:
|
|
||||||
candidate = tpl_dir / "vscode-settings.json"
|
|
||||||
if candidate.is_file():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _merge_vscode_settings(src: Path, dst: Path) -> None:
|
|
||||||
"""Merge settings from *src* into existing *dst* JSON file.
|
|
||||||
|
|
||||||
Top-level keys from *src* are added only if missing in *dst*.
|
|
||||||
For dict-valued keys, sub-keys are merged the same way.
|
|
||||||
|
|
||||||
If *dst* cannot be parsed (e.g. JSONC with comments), the merge
|
|
||||||
is skipped to avoid overwriting user settings.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
existing = json.loads(dst.read_text(encoding="utf-8"))
|
|
||||||
except (json.JSONDecodeError, OSError):
|
|
||||||
# Cannot parse existing file (likely JSONC with comments).
|
|
||||||
# Skip merge to preserve the user's settings, but show
|
|
||||||
# what they should add manually.
|
|
||||||
import logging
|
|
||||||
template_content = src.read_text(encoding="utf-8")
|
|
||||||
logging.getLogger(__name__).warning(
|
|
||||||
"Could not parse %s (may contain JSONC comments). "
|
|
||||||
"Skipping settings merge to preserve existing file.\n"
|
|
||||||
"Please add the following settings manually:\n%s",
|
|
||||||
dst, template_content,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
new_settings = json.loads(src.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
if not isinstance(existing, dict) or not isinstance(new_settings, dict):
|
|
||||||
import logging
|
|
||||||
logging.getLogger(__name__).warning(
|
|
||||||
"Skipping settings merge: %s or template is not a JSON object.", dst
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
changed = False
|
|
||||||
for key, value in new_settings.items():
|
|
||||||
if key not in existing:
|
|
||||||
existing[key] = value
|
|
||||||
changed = True
|
|
||||||
elif isinstance(existing[key], dict) and isinstance(value, dict):
|
|
||||||
for sub_key, sub_value in value.items():
|
|
||||||
if sub_key not in existing[key]:
|
|
||||||
existing[key][sub_key] = sub_value
|
|
||||||
changed = True
|
|
||||||
|
|
||||||
if not changed:
|
|
||||||
return
|
|
||||||
|
|
||||||
dst.write_text(
|
|
||||||
json.dumps(existing, indent=4) + "\n", encoding="utf-8"
|
|
||||||
)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# update-context.ps1 — Copilot integration: create/update .github/copilot-instructions.md
|
|
||||||
#
|
|
||||||
# This is the copilot-specific implementation that produces the GitHub
|
|
||||||
# Copilot instructions file. The shared dispatcher reads
|
|
||||||
# .specify/integration.json and calls this script.
|
|
||||||
#
|
|
||||||
# NOTE: This script is not yet active. It will be activated in Stage 7
|
|
||||||
# when the shared update-agent-context.ps1 replaces its switch statement
|
|
||||||
# with integration.json-based dispatch. The shared script must also be
|
|
||||||
# refactored to support SPECKIT_SOURCE_ONLY (guard the Main call) before
|
|
||||||
# dot-sourcing will work.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Invoke shared update-agent-context script as a separate process.
|
|
||||||
# Dot-sourcing is unsafe until that script guards its Main call.
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType copilot
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# update-context.sh — Copilot integration: create/update .github/copilot-instructions.md
|
|
||||||
#
|
|
||||||
# This is the copilot-specific implementation that produces the GitHub
|
|
||||||
# Copilot instructions file. The shared dispatcher reads
|
|
||||||
# .specify/integration.json and calls this script.
|
|
||||||
#
|
|
||||||
# NOTE: This script is not yet active. It will be activated in Stage 7
|
|
||||||
# when the shared update-agent-context.sh replaces its case statement
|
|
||||||
# with integration.json-based dispatch. The shared script must also be
|
|
||||||
# refactored to support SPECKIT_SOURCE_ONLY (guard the main logic)
|
|
||||||
# before sourcing will work.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
_script_dir="$(cd "$(dirname "$0")" && pwd)"
|
|
||||||
_root="$_script_dir"
|
|
||||||
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
|
|
||||||
if [ -z "${REPO_ROOT:-}" ]; then
|
|
||||||
if [ -d "$_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
else
|
|
||||||
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
||||||
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
|
|
||||||
REPO_ROOT="$git_root"
|
|
||||||
else
|
|
||||||
REPO_ROOT="$_root"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Invoke shared update-agent-context script as a separate process.
|
|
||||||
# Sourcing is unsafe until that script guards its main logic.
|
|
||||||
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" copilot
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
"""Cursor IDE integration."""
|
|
||||||
|
|
||||||
from ..base import MarkdownIntegration
|
|
||||||
|
|
||||||
|
|
||||||
class CursorAgentIntegration(MarkdownIntegration):
|
|
||||||
key = "cursor-agent"
|
|
||||||
config = {
|
|
||||||
"name": "Cursor",
|
|
||||||
"folder": ".cursor/",
|
|
||||||
"commands_subdir": "commands",
|
|
||||||
"install_url": None,
|
|
||||||
"requires_cli": False,
|
|
||||||
}
|
|
||||||
registrar_config = {
|
|
||||||
"dir": ".cursor/commands",
|
|
||||||
"format": "markdown",
|
|
||||||
"args": "$ARGUMENTS",
|
|
||||||
"extension": ".md",
|
|
||||||
}
|
|
||||||
context_file = ".cursor/rules/specify-rules.mdc"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# update-context.ps1 — Cursor integration: create/update .cursor/rules/specify-rules.mdc
|
|
||||||
#
|
|
||||||
# Thin wrapper that delegates to the shared update-agent-context script.
|
|
||||||
# Activated in Stage 7 when the shared script uses integration.json dispatch.
|
|
||||||
#
|
|
||||||
# Until then, this delegates to the shared script as a subprocess.
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
# Derive repo root from script location (walks up to find .specify/)
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
|
|
||||||
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
|
|
||||||
# If git did not return a repo root, or the git root does not contain .specify,
|
|
||||||
# fall back to walking up from the script directory to find the initialized project root.
|
|
||||||
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = $scriptDir
|
|
||||||
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
|
|
||||||
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
|
|
||||||
$repoRoot = Split-Path -Parent $repoRoot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType cursor-agent
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user