Compare commits

..

13 Commits

Author SHA1 Message Date
danielmeppiel
f6cae496a9 Add apm_cli to pyproject.toml and fix apm module url validation 2025-09-17 14:31:54 +02:00
Daniel Meppiel
f9dc5f63b9 Potential fix for code scanning alert no. 1: Incomplete URL substring sanitization
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-16 17:30:17 +02:00
danielmeppiel
229193e488 Strengthens APM url module parsing as per PR review 2025-09-16 17:15:02 +02:00
Daniel Meppiel
6a2f1950ae Update templates/apm/hello-world/apm.yml
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-16 16:55:24 +02:00
Daniel Meppiel
b5092a9dba Update src/apm_cli/core/script_runner.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-16 16:54:19 +02:00
danielmeppiel
fd77f82760 Implementing Copilot suggested fixes from the PR 2025-09-16 16:51:36 +02:00
danielmeppiel
a9512e00fc Fixes as per PR Review from Copilot 2025-09-16 16:08:42 +02:00
Daniel Meppiel
794515d242 Update src/apm_cli/models/apm_package.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-16 13:59:41 +02:00
Daniel Meppiel
93bf878908 Update src/apm_cli/utils/console.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-16 13:56:00 +02:00
danielmeppiel
d501ed6939 Update CODEOWNERS to assign APM CLI ownership to @danielmeppiel 2025-09-16 13:53:31 +02:00
danielmeppiel
52da4ce9d5 Add APM docs 2025-09-16 12:51:26 +02:00
danielmeppiel
6e4f287913 Add prune, uninstall and init --with-apm. Fixes APM package collisions (APM packages with same repo name different owner are now handled well) 2025-09-16 10:24:12 +02:00
danielmeppiel
9d449539bb Add APM CLI - init, install, compile, deps 2025-09-15 23:43:38 +02:00
160 changed files with 15986 additions and 18013 deletions

View File

@@ -1,77 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
"name": "SpecKitDevContainer",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/python:3.13-trixie", // based on Debian "Trixie" (13)
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"installOhMyZsh": true,
"installOhMyZshConfig": true,
"upgradePackages": true,
"username": "devcontainer",
"userUid": "automatic",
"userGid": "automatic"
},
"ghcr.io/devcontainers/features/dotnet:2": {
"version": "lts"
},
"ghcr.io/devcontainers/features/git:1": {
"ppa": true,
"version": "latest"
},
"ghcr.io/devcontainers/features/node": {
"version": "lts"
}
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [
8080 // for Spec-Kit documentation site
],
"containerUser": "devcontainer",
"updateRemoteUserUID": true,
"postCreateCommand": "chmod +x ./.devcontainer/post-create.sh && ./.devcontainer/post-create.sh",
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
"customizations": {
"vscode": {
"extensions": [
"mhutchie.git-graph",
"eamodio.gitlens",
"anweber.reveal-button",
"chrisdias.promptboost",
// Github Copilot
"GitHub.copilot",
"GitHub.copilot-chat",
// Codex
"openai.chatgpt",
// Kilo Code
"kilocode.Kilo-Code",
// Roo Code
"RooVeterinaryInc.roo-cline",
// Amazon Developer Q
"AmazonWebServices.amazon-q-vscode",
// Claude Code
"anthropic.claude-code"
],
"settings": {
"debug.javascript.autoAttachFilter": "disabled", // fix running commands in integrated terminal
// Specify settings for Github Copilot
"git.autofetch": true,
"chat.promptFilesRecommendations": {
"speckit.constitution": true,
"speckit.specify": true,
"speckit.plan": true,
"speckit.tasks": true,
"speckit.implement": true
},
"chat.tools.terminal.autoApprove": {
".specify/scripts/bash/": true,
".specify/scripts/powershell/": true
}
}
}
}
}

View File

@@ -1,100 +0,0 @@
#!/bin/bash
# Exit immediately on error, treat unset variables as an error, and fail if any command in a pipeline fails.
set -euo pipefail
# Function to run a command and show logs only on error
run_command() {
local command_to_run="$*"
local output
local exit_code
# Capture all output (stdout and stderr)
output=$(eval "$command_to_run" 2>&1) || exit_code=$?
exit_code=${exit_code:-0}
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$output\033[0m" >&2
exit $exit_code
fi
}
# Installing CLI-based AI Agents
echo -e "\n🤖 Installing Copilot CLI..."
run_command "npm install -g @github/copilot@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Claude CLI..."
run_command "npm install -g @anthropic-ai/claude-code@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Codex CLI..."
run_command "npm install -g @openai/codex@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Gemini CLI..."
run_command "npm install -g @google/gemini-cli@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Augie CLI..."
run_command "npm install -g @augmentcode/auggie@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Qwen Code CLI..."
run_command "npm install -g @qwen-code/qwen-code@latest"
echo "✅ Done"
echo -e "\n🤖 Installing OpenCode CLI..."
run_command "npm install -g opencode-ai@latest"
echo "✅ Done"
echo -e "\n🤖 Installing Amazon Q CLI..."
# 👉🏾 https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-verify-download.html
run_command "curl --proto '=https' --tlsv1.2 -sSf 'https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip' -o 'q.zip'"
run_command "curl --proto '=https' --tlsv1.2 -sSf 'https://desktop-release.q.us-east-1.amazonaws.com/latest/q-x86_64-linux.zip.sig' -o 'q.zip.sig'"
cat > amazonq-public-key.asc << 'EOF'
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZig60RYJKwYBBAHaRw8BAQdAy/+G05U5/EOA72WlcD4WkYn5SInri8pc4Z6D
BKNNGOm0JEFtYXpvbiBRIENMSSBUZWFtIDxxLWNsaUBhbWF6b24uY29tPoiZBBMW
CgBBFiEEmvYEF+gnQskUPgPsUNx6jcJMVmcFAmYoOtECGwMFCQPCZwAFCwkIBwIC
IgIGFQoJCAsCBBYCAwECHgcCF4AACgkQUNx6jcJMVmef5QD/QWWEGG/cOnbDnp68
SJXuFkwiNwlH2rPw9ZRIQMnfAS0A/0V6ZsGB4kOylBfc7CNfzRFGtovdBBgHqA6P
zQ/PNscGuDgEZig60RIKKwYBBAGXVQEFAQEHQC4qleONMBCq3+wJwbZSr0vbuRba
D1xr4wUPn4Avn4AnAwEIB4h+BBgWCgAmFiEEmvYEF+gnQskUPgPsUNx6jcJMVmcF
AmYoOtECGwwFCQPCZwAACgkQUNx6jcJMVmchMgEA6l3RveCM0YHAGQaSFMkguoAo
vK6FgOkDawgP0NPIP2oA/jIAO4gsAntuQgMOsPunEdDeji2t+AhV02+DQIsXZpoB
=f8yY
-----END PGP PUBLIC KEY BLOCK-----
EOF
run_command "gpg --batch --import amazonq-public-key.asc"
run_command "gpg --verify q.zip.sig q.zip"
run_command "unzip -q q.zip"
run_command "chmod +x ./q/install.sh"
run_command "./q/install.sh --no-confirm"
run_command "rm -rf ./q q.zip q.zip.sig amazonq-public-key.asc"
echo "✅ Done"
echo -e "\n🤖 Installing CodeBuddy CLI..."
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
echo "✅ Done"
# Installing UV (Python package manager)
echo -e "\n🐍 Installing UV - Python Package Manager..."
run_command "pipx install uv"
echo "✅ Done"
# Installing DocFx (for documentation site)
echo -e "\n📚 Installing DocFx..."
run_command "dotnet tool update -g docfx"
echo "✅ Done"
echo -e "\n🧹 Cleaning cache..."
run_command "sudo apt-get autoclean"
run_command "sudo apt-get clean"
echo "✅ Setup completed. Happy coding! 🚀"

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
* text=auto eol=lf

6
.github/CODEOWNERS vendored
View File

@@ -1,3 +1,7 @@
# Global code owner
* @mnriem
* @localden
# APM CLI code owner
src/apm_cli/ @danielmeppiel
templates/apm/ @danielmeppiel
docs/context-management.md @danielmeppiel

View File

@@ -1,141 +0,0 @@
name: Agent Request
description: Request support for a new AI agent/assistant in Spec Kit
title: "[Agent]: Add support for "
labels: ["agent-request", "enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
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, Amazon Q Developer CLI, Amp, SHAI, IBM Bob, Antigravity
- type: input
id: agent-name
attributes:
label: Agent Name
description: What is the name of the AI agent/assistant?
placeholder: "e.g., SuperCoder AI"
validations:
required: true
- type: input
id: website
attributes:
label: Official Website
description: Link to the agent's official website or documentation
placeholder: "https://..."
validations:
required: true
- type: dropdown
id: agent-type
attributes:
label: Agent Type
description: How is the agent accessed?
options:
- CLI tool (command-line interface)
- IDE extension/plugin
- Both CLI and IDE
- Other
validations:
required: true
- type: input
id: cli-command
attributes:
label: CLI Command (if applicable)
description: What command is used to invoke the agent from terminal?
placeholder: "e.g., supercode, ai-assistant"
- type: input
id: install-method
attributes:
label: Installation Method
description: How is the agent installed?
placeholder: "e.g., npm install -g supercode, pip install supercode, IDE marketplace"
validations:
required: true
- type: textarea
id: command-structure
attributes:
label: Command/Workflow Structure
description: How does the agent define custom commands or workflows?
placeholder: |
- Command file format (Markdown, YAML, TOML, etc.)
- Directory location (e.g., .supercode/commands/)
- Example command file structure
validations:
required: true
- type: textarea
id: argument-pattern
attributes:
label: Argument Passing Pattern
description: How does the agent handle arguments in commands?
placeholder: |
e.g., Uses {{args}}, $ARGUMENTS, %ARGS%, or other placeholder format
Example: "Run test suite with {{args}}"
- type: dropdown
id: popularity
attributes:
label: Popularity/Usage
description: How widely is this agent used?
options:
- Widely used (thousands+ of users)
- Growing adoption (hundreds of users)
- New/emerging (less than 100 users)
- Unknown
validations:
required: true
- type: textarea
id: documentation
attributes:
label: Documentation Links
description: Links to relevant documentation for custom commands/workflows
placeholder: |
- Command documentation: https://...
- API/CLI reference: https://...
- Examples: https://...
- type: textarea
id: use-case
attributes:
label: Use Case
description: Why do you want this agent supported in Spec Kit?
placeholder: Explain your workflow and how this agent fits into your development process
validations:
required: true
- type: textarea
id: example-command
attributes:
label: Example Command File
description: If possible, provide an example of a command file for this agent
render: markdown
placeholder: |
```toml
description = "Example command"
prompt = "Do something with {{args}}"
```
- type: checkboxes
id: contribution
attributes:
label: Contribution
description: Are you willing to help implement support for this agent?
options:
- label: I can help test the integration
- label: I can provide example command files
- label: I can help with documentation
- label: I can submit a pull request for the integration
- type: textarea
id: context
attributes:
label: Additional Context
description: Any other relevant information about this agent
placeholder: Screenshots, community links, comparison to existing agents, etc.

View File

@@ -1,118 +0,0 @@
name: Bug Report
description: Report a bug or unexpected behavior in Specify CLI or Spec Kit
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug! Please fill out the sections below to help us diagnose and fix the issue.
- type: textarea
id: description
attributes:
label: Bug Description
description: A clear and concise description of what the bug is.
placeholder: What went wrong?
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to Reproduce
description: Steps to reproduce the behavior
placeholder: |
1. Run command '...'
2. Execute script '...'
3. See error
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: What did you expect to happen?
placeholder: Describe the expected outcome
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
description: What actually happened?
placeholder: Describe what happened instead
validations:
required: true
- type: input
id: version
attributes:
label: Specify CLI Version
description: "Run `specify version` or `pip show spec-kit`"
placeholder: "e.g., 1.3.0"
validations:
required: true
- type: dropdown
id: ai-agent
attributes:
label: AI Agent
description: Which AI agent are you using?
options:
- Claude Code
- Gemini CLI
- GitHub Copilot
- Cursor
- Qwen Code
- opencode
- Codex CLI
- Windsurf
- Kilo Code
- Auggie CLI
- Roo Code
- CodeBuddy
- Qoder CLI
- Amazon Q Developer CLI
- Amp
- SHAI
- IBM Bob
- Antigravity
- Not applicable
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
description: Your operating system and version
placeholder: "e.g., macOS 14.2, Ubuntu 22.04, Windows 11"
validations:
required: true
- type: input
id: python
attributes:
label: Python Version
description: "Run `python --version` or `python3 --version`"
placeholder: "e.g., Python 3.11.5"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Error Logs
description: Please paste any relevant error messages or logs
render: shell
placeholder: Paste error output here
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context about the problem
placeholder: Screenshots, related issues, workarounds attempted, etc.

View File

@@ -1,17 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: 💬 General Discussion
url: https://github.com/github/spec-kit/discussions
about: Ask questions, share ideas, or discuss Spec-Driven Development
- name: 📖 Documentation
url: https://github.com/github/spec-kit/blob/main/README.md
about: Read the Spec Kit documentation and guides
- name: 🛠️ Extension Development Guide
url: https://github.com/manfredseee/spec-kit/blob/main/extensions/EXTENSION-DEVELOPMENT-GUIDE.md
about: Learn how to develop and publish Spec Kit extensions
- name: 🤝 Contributing Guide
url: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md
about: Learn how to contribute to Spec Kit
- name: 🔒 Security Issues
url: https://github.com/github/spec-kit/blob/main/SECURITY.md
about: Report security vulnerabilities privately

View File

@@ -1,280 +0,0 @@
name: Extension Submission
description: Submit your extension to the Spec Kit catalog
title: "[Extension]: Add "
labels: ["extension-submission", "enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for contributing an extension! This template helps you submit your extension to the community catalog.
**Before submitting:**
- 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
- Create a GitHub release with a version tag (e.g., v1.0.0)
- Test installation: `specify extension add --from <your-release-url>`
- type: input
id: extension-id
attributes:
label: Extension ID
description: Unique extension identifier (lowercase with hyphens only)
placeholder: "e.g., jira-integration"
validations:
required: true
- type: input
id: extension-name
attributes:
label: Extension Name
description: Human-readable extension name
placeholder: "e.g., Jira Integration"
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 extension does (under 200 characters)
placeholder: Integrates Jira issue tracking with Spec Kit workflows for seamless task management
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 extension
placeholder: "https://github.com/your-org/spec-kit-your-extension"
validations:
required: true
- type: input
id: download-url
attributes:
label: Download URL
description: URL to the GitHub release archive (e.g., v1.0.0.zip)
placeholder: "https://github.com/your-org/spec-kit-your-extension/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: homepage
attributes:
label: Homepage (optional)
description: Link to extension homepage or documentation site
placeholder: "https://..."
- type: input
id: documentation
attributes:
label: Documentation URL (optional)
description: Link to detailed documentation
placeholder: "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/"
- type: input
id: changelog
attributes:
label: Changelog URL (optional)
description: Link to changelog file
placeholder: "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md"
- type: input
id: speckit-version
attributes:
label: Required Spec Kit Version
description: Minimum Spec Kit version required
placeholder: "e.g., >=0.1.0"
validations:
required: true
- type: textarea
id: required-tools
attributes:
label: Required Tools (optional)
description: List any external tools or dependencies required
placeholder: |
- jira-cli (>=1.0.0) - required
- python (>=3.8) - optional
render: markdown
- type: input
id: commands-count
attributes:
label: Number of Commands
description: How many commands does your extension provide?
placeholder: "e.g., 3"
validations:
required: true
- type: input
id: hooks-count
attributes:
label: Number of Hooks (optional)
description: How many hooks does your extension provide?
placeholder: "e.g., 0"
- type: textarea
id: tags
attributes:
label: Tags
description: 2-5 relevant tags (lowercase, separated by commas)
placeholder: "issue-tracking, jira, atlassian, automation"
validations:
required: true
- type: textarea
id: features
attributes:
label: Key Features
description: List the main features and capabilities of your extension
placeholder: |
- Create Jira issues from specs
- Sync task status with Jira
- Link specs to existing issues
- Generate Jira reports
validations:
required: true
- type: checkboxes
id: testing
attributes:
label: Testing Checklist
description: Confirm that your extension has been tested
options:
- label: Extension installs successfully via download URL
required: true
- label: All commands execute without errors
required: true
- label: Documentation is complete and accurate
required: true
- label: No security vulnerabilities identified
required: true
- label: Tested on at least one real project
required: true
- type: checkboxes
id: requirements
attributes:
label: Submission Requirements
description: Verify your extension meets all requirements
options:
- label: Valid `extension.yml` manifest included
required: true
- label: README.md with installation and usage instructions
required: true
- label: LICENSE file included
required: true
- label: GitHub release created with version tag
required: true
- label: All command files exist and are properly formatted
required: true
- label: Extension ID follows naming conventions (lowercase-with-hyphens)
required: true
- type: textarea
id: testing-details
attributes:
label: Testing Details
description: Describe how you tested your extension
placeholder: |
**Tested on:**
- macOS 14.0 with Spec Kit v0.1.0
- Linux Ubuntu 22.04 with Spec Kit v0.1.0
**Test project:** [Link or description]
**Test scenarios:**
1. Installed extension
2. Configured settings
3. Ran all commands
4. Verified outputs
validations:
required: true
- type: textarea
id: example-usage
attributes:
label: Example Usage
description: Provide a simple example of using your extension
render: markdown
placeholder: |
```bash
# Install extension
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
# Use a command
/speckit.your-extension.command-name arg1 arg2
```
validations:
required: true
- type: textarea
id: catalog-entry
attributes:
label: Proposed Catalog Entry
description: Provide the JSON entry for catalog.json (helps reviewers)
render: json
placeholder: |
{
"your-extension": {
"name": "Your Extension",
"id": "your-extension",
"description": "Brief description",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-your-extension",
"homepage": "https://github.com/your-org/spec-kit-your-extension",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 3
},
"tags": ["category", "tool"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-20T00:00:00Z",
"updated_at": "2026-02-20T00:00:00Z"
}
}
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other information that would help reviewers
placeholder: Screenshots, demo videos, links to related projects, etc.

View File

@@ -1,104 +0,0 @@
name: Feature Request
description: Suggest a new feature or enhancement for Specify CLI or Spec Kit
title: "[Feature]: "
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for suggesting a feature! Please provide details below to help us understand and evaluate your request.
- type: textarea
id: problem
attributes:
label: Problem Statement
description: Is your feature request related to a problem? Please describe.
placeholder: "I'm frustrated when..."
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: Describe the solution you'd like
placeholder: What would you like to happen?
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Have you considered any alternative solutions or workarounds?
placeholder: What other approaches might work?
- type: dropdown
id: component
attributes:
label: Component
description: Which component does this feature relate to?
options:
- Specify CLI (initialization, commands)
- Spec templates (BDD, Testing Strategy, etc.)
- Agent integrations (command files, workflows)
- Scripts (Bash/PowerShell utilities)
- Documentation
- CI/CD workflows
- Other
validations:
required: true
- type: dropdown
id: ai-agent
attributes:
label: AI Agent (if applicable)
description: Does this feature relate to a specific AI agent?
options:
- All agents
- Claude Code
- Gemini CLI
- GitHub Copilot
- Cursor
- Qwen Code
- opencode
- Codex CLI
- Windsurf
- Kilo Code
- Auggie CLI
- Roo Code
- CodeBuddy
- Qoder CLI
- Amazon Q Developer CLI
- Amp
- SHAI
- IBM Bob
- Antigravity
- Not applicable
- type: textarea
id: use-cases
attributes:
label: Use Cases
description: Describe specific use cases where this feature would be valuable
placeholder: |
1. When working on large projects...
2. During spec review...
3. When integrating with CI/CD...
- type: textarea
id: acceptance
attributes:
label: Acceptance Criteria
description: How would you know this feature is complete and working?
placeholder: |
- [ ] Feature does X
- [ ] Documentation is updated
- [ ] Works with all supported agents
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context, screenshots, or examples
placeholder: Links to similar features, mockups, related discussions, etc.

View File

@@ -1,22 +0,0 @@
## Description
<!-- What does this PR do? Why is it needed? -->
## Testing
<!-- How did you test your changes? -->
- [ ] Tested locally with `uv run specify --help`
- [ ] Ran existing tests with `uv sync && uv run pytest`
- [ ] Tested with a sample project (if applicable)
## AI Disclosure
<!-- Per our Contributing guidelines, AI assistance must be disclosed. -->
<!-- See: https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md#ai-contributions-in-spec-kit -->
- [ ] I **did not** use AI assistance for this contribution
- [ ] I **did** use AI assistance (describe below)
<!-- If you used AI, briefly describe how (e.g., "Code generated by Copilot", "Consulted ChatGPT for approach"): -->

View File

@@ -1,11 +0,0 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,32 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
strategy:
fail-fast: false
matrix:
language: [ 'actions', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"

View File

@@ -65,4 +65,3 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,22 +0,0 @@
name: Lint
permissions:
contents: read
on:
push:
branches: ["main"]
pull_request:
jobs:
markdownlint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: |
'**/*.md'
!extensions/**/*.md

View File

@@ -6,7 +6,6 @@ on:
paths:
- 'memory/**'
- 'scripts/**'
- 'src/**'
- 'templates/**'
- '.github/workflows/**'
workflow_dispatch:
@@ -26,13 +25,33 @@ jobs:
- name: Get latest tag
id: get_tag
run: |
chmod +x .github/workflows/scripts/get-next-version.sh
.github/workflows/scripts/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"
- name: Check if release already exists
id: check_release
run: |
chmod +x .github/workflows/scripts/check-release-exists.sh
.github/workflows/scripts/check-release-exists.sh ${{ steps.get_tag.outputs.new_version }}
if gh release view ${{ steps.get_tag.outputs.new_version }} >/dev/null 2>&1; then
echo "exists=true" >> $GITHUB_OUTPUT
echo "Release ${{ steps.get_tag.outputs.new_version }} already exists, skipping..."
else
echo "exists=false" >> $GITHUB_OUTPUT
echo "Release ${{ steps.get_tag.outputs.new_version }} does not exist, proceeding..."
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release package variants
@@ -44,26 +63,69 @@ jobs:
if: steps.check_release.outputs.exists == 'false'
id: release_notes
run: |
chmod +x .github/workflows/scripts/generate-release-notes.sh
.github/workflows/scripts/generate-release-notes.sh ${{ steps.get_tag.outputs.new_version }} ${{ steps.get_tag.outputs.latest_tag }}
# Get commits since last tag
LAST_TAG=${{ steps.get_tag.outputs.latest_tag }}
if [ "$LAST_TAG" = "v0.0.0" ]; then
# Check how many commits we have and use that as the limit
COMMIT_COUNT=$(git rev-list --count HEAD)
if [ "$COMMIT_COUNT" -gt 10 ]; then
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~10..HEAD)
else
COMMITS=$(git log --oneline --pretty=format:"- %s" HEAD~$COMMIT_COUNT..HEAD 2>/dev/null || git log --oneline --pretty=format:"- %s")
fi
else
COMMITS=$(git log --oneline --pretty=format:"- %s" $LAST_TAG..HEAD)
fi
# Create release notes
cat > release_notes.md << EOF
Template release ${{ steps.get_tag.outputs.new_version }}
Updated specification-driven development templates for GitHub Copilot, Claude Code, Gemini CLI, and Cursor.
Now includes per-script variants for POSIX shell (sh) and PowerShell (ps).
Download the template for your preferred AI assistant + script type:
- spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip
- spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip
EOF
echo "Generated release notes:"
cat release_notes.md
- name: Create GitHub Release
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/create-github-release.sh
.github/workflows/scripts/create-github-release.sh ${{ steps.get_tag.outputs.new_version }}
# Remove 'v' prefix from version for release title
VERSION_NO_V=${{ steps.get_tag.outputs.new_version }}
VERSION_NO_V=${VERSION_NO_V#v}
gh release create ${{ steps.get_tag.outputs.new_version }} \
spec-kit-template-copilot-sh-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-copilot-ps-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-claude-sh-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-claude-ps-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-gemini-sh-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-gemini-ps-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-cursor-sh-${{ steps.get_tag.outputs.new_version }}.zip \
spec-kit-template-cursor-ps-${{ steps.get_tag.outputs.new_version }}.zip \
--title "Spec Kit Templates - $VERSION_NO_V" \
--notes-file release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update version in pyproject.toml (for release artifacts only)
if: steps.check_release.outputs.exists == 'false'
run: |
chmod +x .github/workflows/scripts/update-version.sh
.github/workflows/scripts/update-version.sh ${{ steps.get_tag.outputs.new_version }}
- name: Commit version bump to main
if: steps.check_release.outputs.exists == 'false'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add pyproject.toml
git diff --cached --quiet || git commit -m "chore: bump version to ${{ steps.get_tag.outputs.new_version }} [skip ci]"
git push
# Update version in pyproject.toml (remove 'v' prefix for Python versioning)
VERSION=${{ steps.get_tag.outputs.new_version }}
PYTHON_VERSION=${VERSION#v}
if [ -f "pyproject.toml" ]; then
sed -i "s/version = \".*\"/version = \"$PYTHON_VERSION\"/" pyproject.toml
echo "Updated pyproject.toml version to $PYTHON_VERSION (for release artifacts only)"
fi

View File

@@ -1,21 +0,0 @@
#!/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

View File

@@ -1,58 +0,0 @@
#!/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-q-sh-"$VERSION".zip \
.genreleases/spec-kit-template-q-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

View File

@@ -1,428 +0,0 @@
#!/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, q, 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
}
'q' {
$cmdDir = Join-Path $baseDir ".amazonq/prompts"
Generate-Commands -Agent 'q' -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
}
'generic' {
$cmdDir = Join-Path $baseDir ".speckit/commands"
Generate-Commands -Agent 'generic' -Extension 'md' -ArgFormat '$ARGUMENTS' -OutputDir $cmdDir -ScriptVariant $Script
}
}
# 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', 'q', '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)"
}

173
.github/workflows/scripts/create-release-packages.sh vendored Executable file → Normal file
View File

@@ -6,7 +6,7 @@ set -euo pipefail
# 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 amp shai bob generic (default: all)
# AGENTS : space or comma separated subset of: claude gemini copilot (default: all)
# SCRIPTS : space or comma separated subset of: sh ps (default: both)
# Examples:
# AGENTS=claude SCRIPTS=sh $0 v0.2.0
@@ -25,17 +25,13 @@ 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
rm -rf sdd-package-base* sdd-*-package-* spec-kit-template-*-${NEW_VERSION}.zip || 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'
-e 's@(/?)templates/@.specify/templates/@g'
}
generate_commands() {
@@ -43,7 +39,7 @@ generate_commands() {
mkdir -p "$output_dir"
for template in templates/commands/*.md; do
[[ -f "$template" ]] || continue
local name description script_command agent_script_command body
local name description script_command body
name=$(basename "$template" .md)
# Normalize line endings
@@ -58,30 +54,13 @@ generate_commands() {
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
# Remove the scripts: section 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 }
@@ -92,39 +71,18 @@ generate_commands() {
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" ;;
{ echo "description = \"$description\""; echo; echo "prompt = \"\"\""; echo "$body"; echo "\"\"\""; } > "$output_dir/$name.$ext" ;;
md)
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
agent.md)
echo "$body" > "$output_dir/speckit.$name.$ext" ;;
echo "$body" > "$output_dir/$name.$ext" ;;
prompt.md)
echo "$body" > "$output_dir/$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}"
local base_dir="sdd-${agent}-package-${script}"
echo "Building $agent ($script) package..."
mkdir -p "$base_dir"
@@ -151,13 +109,19 @@ build_variant() {
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.
[[ -d templates ]] && { mkdir -p "$SPEC_DIR/templates"; find templates -type f -not -path "templates/commands/*" -exec cp --parents {} "$SPEC_DIR"/ \; ; echo "Copied templates -> .specify/templates"; }
# Inject variant into plan-template.md within .specify/templates if present
local plan_tpl="$base_dir/.specify/templates/plan-template.md"
if [[ -f "$plan_tpl" ]]; then
plan_norm=$(tr -d '\r' < "$plan_tpl")
variant_line=$(printf '%s\n' "$plan_norm" | grep -E "<!--[[:space:]]*VARIANT:$script" | head -1 | sed -E "s/.*VARIANT:$script[[:space:]]+//; s/-->.*//; s/^[[:space:]]+//; s/[[:space:]]+$//")
if [[ -n $variant_line ]]; then
tmp_file=$(mktemp)
sed "s|VARIANT-INJECT|${variant_line}|" "$plan_tpl" | tr -d '\r' | sed "s|__AGENT__|${agent}|g" | sed '/<!--[[:space:]]*VARIANT:sh/d' | sed '/<!--[[:space:]]*VARIANT:ps/d' > "$tmp_file" && mv "$tmp_file" "$plan_tpl"
else
echo "Warning: no plan-template variant for $script (pattern not matched)" >&2
fi
fi
case $agent in
claude)
mkdir -p "$base_dir/.claude/commands"
@@ -167,103 +131,51 @@ build_variant() {
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/.github/prompts"
generate_commands copilot prompt.md "\$ARGUMENTS" "$base_dir/.github/prompts" "$script" ;;
cursor)
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" ;;
q)
mkdir -p "$base_dir/.amazonq/prompts"
generate_commands q md "\$ARGUMENTS" "$base_dir/.amazonq/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" ;;
generate_commands cursor md "\$ARGUMENTS" "$base_dir/.cursor/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"
echo "Created 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 q agy bob qodercli generic)
ALL_AGENTS=(claude gemini copilot cursor)
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")}'
# convert comma+space separated -> space separated unique while preserving order of first occurrence
tr ',\n' ' ' | awk '{for(i=1;i<=NF;i++){if(!seen[$i]++){printf((out?" ":"") $i)}}}END{printf("\n")}'
}
validate_subset() {
local type=$1; shift; local -n allowed=$1; shift; local items=("$@")
local invalid=0
local type=$1; shift; local -n allowed=$1; shift; local items=($@)
local ok=1
for it in "${items[@]}"; do
local found=0
for a in "${allowed[@]}"; do [[ $it == "$a" ]] && { found=1; break; }; done
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
ok=0
fi
done
return $invalid
return $ok
}
if [[ -n ${AGENTS:-} ]]; then
mapfile -t AGENT_LIST < <(printf '%s' "$AGENTS" | norm_list)
AGENT_LIST=($(printf '%s' "$AGENTS" | norm_list))
validate_subset agent ALL_AGENTS "${AGENT_LIST[@]}" || exit 1
else
AGENT_LIST=("${ALL_AGENTS[@]}")
AGENT_LIST=(${ALL_AGENTS[@]})
fi
if [[ -n ${SCRIPTS:-} ]]; then
mapfile -t SCRIPT_LIST < <(printf '%s' "$SCRIPTS" | norm_list)
SCRIPT_LIST=($(printf '%s' "$SCRIPTS" | norm_list))
validate_subset script ALL_SCRIPTS "${SCRIPT_LIST[@]}" || exit 1
else
SCRIPT_LIST=("${ALL_SCRIPTS[@]}")
SCRIPT_LIST=(${ALL_SCRIPTS[@]})
fi
echo "Agents: ${AGENT_LIST[*]}"
@@ -275,6 +187,5 @@ for agent in "${AGENT_LIST[@]}"; do
done
done
echo "Archives in $GENRELEASES_DIR:"
ls -1 "$GENRELEASES_DIR"/spec-kit-template-*-"${NEW_VERSION}".zip
echo "Archives:"
ls -1 spec-kit-template-*-${NEW_VERSION}.zip

View File

@@ -1,40 +0,0 @@
#!/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

View File

@@ -1,24 +0,0 @@
#!/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"

View File

@@ -1,23 +0,0 @@
#!/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

View File

@@ -1,42 +0,0 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '0 0 * * *' # Run daily at midnight UTC
workflow_dispatch: # Allow manual triggering
permissions:
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
# Days of inactivity before an issue or PR becomes stale
days-before-stale: 150
# Days of inactivity before a stale issue or PR is closed (after being marked stale)
days-before-close: 30
# Stale issue settings
stale-issue-message: 'This issue has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-issue-message: 'This issue has been automatically closed due to inactivity (180 days total). If you believe this issue is still relevant, please reopen it or create a new issue.'
stale-issue-label: 'stale'
# Stale PR settings
stale-pr-message: 'This pull request has been automatically marked as stale because it has not had any activity for 150 days. It will be closed in 30 days if no further activity occurs.'
close-pr-message: 'This pull request has been automatically closed due to inactivity (180 days total). If you believe this PR is still relevant, please reopen it or create a new PR.'
stale-pr-label: 'stale'
# Exempt issues and PRs with these labels from being marked as stale
exempt-issue-labels: 'pinned,security'
exempt-pr-labels: 'pinned,security'
# Only issues or PRs with all of these labels are checked
# Leave empty to check all issues and PRs
any-of-labels: ''
# Operations per run (helps avoid rate limits)
operations-per-run: 100

View File

@@ -1,50 +0,0 @@
name: Test & Lint Python
permissions:
contents: read
on:
push:
branches: ["main"]
pull_request:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Run ruff check
run: uvx ruff check src/
pytest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --extra test
- name: Run tests
run: uv run pytest

12
.gitignore vendored
View File

@@ -32,21 +32,9 @@ env/
*.swp
*.swo
.DS_Store
*.tmp
# Project specific
*.log
.env
.env.local
*.lock
# Spec Kit-specific files
.genreleases/
*.zip
sdd-*/
docs/dev
# Extension system
.specify/extensions/.cache/
.specify/extensions/.backup/
.specify/extensions/*/local-config.yml

View File

@@ -1,29 +0,0 @@
{
// https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
"config": {
"default": true,
"MD003": {
"style": "atx"
},
"MD007": {
"indent": 2
},
"MD013": false,
"MD024": {
"siblings_only": true
},
"MD033": false,
"MD041": false,
"MD049": {
"style": "asterisk"
},
"MD050": {
"style": "asterisk"
},
"MD036": false,
"MD060": false
},
"ignores": [
".genreleases/"
]
}

419
AGENTS.md
View File

@@ -1,419 +0,0 @@
# AGENTS.md
## About Spec Kit and Specify
**GitHub Spec Kit** is a comprehensive toolkit for implementing Spec-Driven Development (SDD) - a methodology that emphasizes creating clear specifications before implementation. The toolkit includes templates, scripts, and workflows that guide development teams through a structured approach to building software.
**Specify CLI** is the command-line interface that bootstraps projects with the Spec Kit framework. It sets up the necessary directory structures, templates, and AI agent integrations to support the Spec-Driven Development workflow.
The toolkit supports multiple AI coding assistants, allowing teams to use their preferred tools while maintaining consistent project structure and development practices.
---
## General practices
- 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
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
Specify supports multiple AI agents by generating agent-specific command files and directory structures when initializing projects. Each agent has its own conventions for:
- **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.)
### Current Supported Agents
| 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 |
| **Amazon Q Developer CLI** | `.amazonq/prompts/` | Markdown | `q` | Amazon Q Developer 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 |
### Step-by-Step Integration Guide
Follow these steps to add a new agent (using a hypothetical new agent as an example):
#### 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
AGENT_CONFIG = {
# ... existing agents ...
"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
"commands_subdir": "commands", # Subdirectory name for command files (default: "commands")
"install_url": "https://example.com/install", # URL for installation docs (or None if IDE-based)
"requires_cli": True, # True if CLI tool required, False for IDE-based agents
},
}
```
**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, q), `"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
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 q"),
```
Also update any function docstrings, examples, and error messages that list available agents.
#### 3. Update README Documentation
Update the **Supported AI Agents** section in `README.md` to include the new agent:
- 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
Modify `.github/workflows/scripts/create-release-packages.sh`:
##### Add to ALL_AGENTS array
```bash
ALL_AGENTS=(claude gemini copilot cursor-agent qwen opencode windsurf q)
```
##### 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
$windsurfFile = Join-Path $repoRoot '.windsurf/rules/specify-rules.md'
```
Add to switch statement:
```powershell
switch ($AgentType) {
# ... existing cases ...
'windsurf' { Update-AgentFile $windsurfFile 'Windsurf' }
'' {
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",
# ...
}
}
# Then you need special cases everywhere:
cli_tool = agent_key
if agent_key == "cursor":
cli_tool = "cursor-agent" # Map to the real tool name
```
**Correct approach** (no mapping needed):
```python
AGENT_CONFIG = {
"cursor-agent": { # Matches the actual executable name
"name": "Cursor",
# ...
}
}
# No special cases needed - just use agent_key directly!
```
**Benefits of this approach:**
- Eliminates special-case logic scattered throughout the codebase
- 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
#### 7. Update Devcontainer files (Optional)
For agents that have VS Code extensions or require CLI installation, update the devcontainer configuration files:
##### VS Code Extension-based Agents
For agents available as VS Code extensions, add them to `.devcontainer/devcontainer.json`:
```json
{
"customizations": {
"vscode": {
"extensions": [
// ... existing extensions ...
// [New Agent Name]
"[New Agent Extension ID]"
]
}
}
}
```
##### CLI-based Agents
For agents that require CLI tools, add installation commands to `.devcontainer/post-create.sh`:
```bash
#!/bin/bash
# Existing installations...
echo -e "\n🤖 Installing [New Agent Name] CLI..."
# 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"
```
**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
- **Amazon Q Developer CLI**: `q` 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
### Markdown Format
Used by: Claude, Cursor, opencode, Windsurf, Amazon Q Developer, Amp, SHAI, IBM Bob
**Standard format:**
```markdown
---
description: "Command description"
---
Command content with {SCRIPT} and $ARGUMENTS placeholders.
```
**GitHub Copilot Chat Mode format:**
```markdown
---
description: "Command description"
mode: speckit.command-name
---
Command content with {SCRIPT} and $ARGUMENTS placeholders.
```
### TOML Format
Used by: Gemini, Qwen
```toml
description = "Command description"
prompt = """
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
Different agents use different argument placeholders:
- **Markdown/prompt-based**: `$ARGUMENTS`
- **TOML-based**: `{{args}}`
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
- **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
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 scripts must be updated when adding new 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 correct placeholder format for each agent type (`$ARGUMENTS` for Markdown, `{{args}}` for TOML).
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 agents are added to maintain accuracy and completeness.*

View File

@@ -1,82 +1,22 @@
# Changelog
<!-- markdownlint-disable MD024 -->
Recent changes to the Specify CLI and templates are documented here.
All notable changes to the Specify CLI will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.6] - 2026-02-23
## [Unreleased]
### Fixed
- **Parameter Ordering Issues (#1641)**: Fixed CLI parameter parsing issue where option flags were incorrectly consumed as values for preceding options
- Added validation to detect when `--ai` or `--ai-commands-dir` incorrectly consume following flags like `--here` or `--ai-skills`
- Now provides clear error messages: "Invalid value for --ai: '--here'"
- Includes helpful hints suggesting proper usage and listing available agents
- Commands like `specify init --ai-skills --ai --here` now fail with actionable feedback instead of confusing "Must specify project name" errors
- Added comprehensive test suite (5 new tests) to prevent regressions
## [0.1.5] - 2026-02-21
### Fixed
- **AI Skills Installation Bug (#1658)**: Fixed `--ai-skills` flag not generating skill files for GitHub Copilot and other agents with non-standard command directory structures
- Added `commands_subdir` field to `AGENT_CONFIG` to explicitly specify the subdirectory name for each agent
- Affected agents now work correctly: copilot (`.github/agents/`), opencode (`.opencode/command/`), windsurf (`.windsurf/workflows/`), codex (`.codex/prompts/`), kilocode (`.kilocode/workflows/`), q (`.amazonq/prompts/`), and agy (`.agent/workflows/`)
- The `install_ai_skills()` function now uses the correct path for all agents instead of assuming `commands/` for everyone
## [0.1.4] - 2026-02-20
### Fixed
- **Qoder CLI detection**: Renamed `AGENT_CONFIG` key from `"qoder"` to `"qodercli"` to match the actual executable name, fixing `specify check` and `specify init --ai` detection failures
## [0.1.3] - 2026-02-20
## [0.0.4] - 2025-09-14
### Added
- **Generic Agent Support**: Added `--ai generic` option for unsupported AI agents ("bring your own agent")
- Requires `--ai-commands-dir <path>` to specify where the agent reads commands from
- Generates Markdown commands with `$ARGUMENTS` format (compatible with most agents)
- Example: `specify init my-project --ai generic --ai-commands-dir .myagent/commands/`
- Enables users to start with Spec Kit immediately while their agent awaits formal support
- SOCKS proxy support for corporate environments via `httpx[socks]` dependency
## [0.0.102] - 2026-02-20
### Fixed
- fix: include 'src/**' path in release workflow triggers (#1646)
N/A
## [0.0.101] - 2026-02-19
### Changed
- chore(deps): bump github/codeql-action from 3 to 4 (#1635)
## [0.0.100] - 2026-02-19
- Add pytest and Python linting (ruff) to CI (#1637)
- feat: add pull request template for better contribution guidelines (#1634)
## [0.0.99] - 2026-02-19
- Feat/ai skills (#1632)
## [0.0.98] - 2026-02-19
- chore(deps): bump actions/stale from 9 to 10 (#1623)
- feat: add dependabot configuration for pip and GitHub Actions updates (#1622)
## [0.0.97] - 2026-02-18
- Remove Maintainers section from README.md (#1618)
## [0.0.96] - 2026-02-17
- fix: typo in plan-template.md (#1446)
## [0.0.95] - 2026-02-12
- Feat: add a new agent: Google Anti Gravity (#1220)
## [0.0.94] - 2026-02-11
- Add stale workflow for 180-day inactive issues and PRs (#1594)
N/A

View File

@@ -14,21 +14,21 @@ orientation.
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
@@ -71,4 +71,4 @@ This Code of Conduct is adapted from the [Contributor Covenant][homepage], versi
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,4 +1,4 @@
# Contributing to Spec Kit
## Contributing to Spec Kit
Hi there! We're thrilled that you'd like to contribute to Spec Kit. Contributions to this project are [released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license) to the public under the [project's open source license](LICENSE).
@@ -11,29 +11,12 @@ These are one time installations required to be able to test your changes locall
1. Install [Python 3.11+](https://www.python.org/downloads/)
1. Install [uv](https://docs.astral.sh/uv/) for package management
1. Install [Git](https://git-scm.com/downloads)
1. Have an [AI coding agent available](README.md#-supported-ai-agents)
<details>
<summary><b>💡 Hint if you are using <code>VSCode</code> or <code>GitHub Codespaces</code> as your IDE</b></summary>
<br>
Provided you have [Docker](https://docker.com) installed on your machine, you can leverage [Dev Containers](https://containers.dev) through this [VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers), to easily set up your development environment, with aforementioned tools already installed and configured, thanks to the `.devcontainer/devcontainer.json` file (located at the root of the project).
To do so, simply:
- Checkout the repo
- Open it with VSCode
- Open the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) and select "Dev Containers: Open Folder in Container..."
On [GitHub Codespaces](https://github.com/features/codespaces) it's even simpler, as it leverages the `.devcontainer/devcontainer.json` automatically upon opening the codespace.
</details>
1. Have an AI coding agent available: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli) are recommended, but we're working on adding support for other agents as well.
## Submitting a pull request
> [!NOTE]
> 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.
>[!NOTE]
>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. Configure and install the dependencies: `uv sync`
@@ -48,7 +31,7 @@ Here are a few things you can do that will increase the likelihood of your pull
- Follow the project's coding conventions.
- Write tests for new functionality.
- Update documentation (`README.md`, `spec-driven.md`) if your changes affect user-facing features.
- Update documentation (`README.md,` `spec-driven.md`) if your changes affect user-facing features.
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
- Test your changes with the Spec-Driven Development workflow to ensure compatibility.
@@ -57,91 +40,11 @@ Here are a few things you can do that will increase the likelihood of your pull
When working on spec-kit:
1. Test changes with the `specify` CLI commands (`/speckit.specify`, `/speckit.plan`, `/speckit.tasks`) in your coding agent of choice
1. Test changes with the `specify` CLI commands (`/specify`, `/plan`, `/tasks`) in your coding agent of choice
2. Verify templates are working correctly in `templates/` directory
3. Test script functionality in the `scripts/` directory
4. Ensure memory files (`memory/constitution.md`) are updated if major process changes are made
### Testing template and command changes locally
Running `uv run specify init` pulls released packages, which wont include your local changes.
To test your templates, commands, and other changes locally, follow these steps:
1. **Create release packages**
Run the following command to generate the local packages:
```bash
./.github/workflows/scripts/create-release-packages.sh v1.0.0
```
2. **Copy the relevant package to your test project**
```bash
cp -r .genreleases/sdd-copilot-package-sh/. <path-to-test-project>/
```
3. **Open and test the agent**
Navigate to your test project folder and open the agent to verify your implementation.
## AI contributions in Spec Kit
> [!IMPORTANT]
>
> If you are using **any kind of AI assistance** to contribute to Spec Kit,
> it must be disclosed in the pull request or issue.
We welcome and encourage the use of AI tools to help improve Spec Kit! Many valuable contributions have been enhanced with AI assistance for code generation, issue detection, and feature definition.
That being said, if you are using any kind of AI assistance (e.g., agents, ChatGPT) while contributing to Spec Kit,
**this must be disclosed in the pull request or issue**, along with the extent to which AI assistance was used (e.g., documentation comments vs. code generation).
If your PR responses or comments are being generated by an AI, disclose that as well.
As an exception, trivial spacing or typo fixes don't need to be disclosed, so long as the changes are limited to small parts of the code or short phrases.
An example disclosure:
> This PR was written primarily by GitHub Copilot.
Or a more detailed disclosure:
> I consulted ChatGPT to understand the codebase but the solution
> was fully authored manually by myself.
Failure to disclose this is first and foremost rude to the human operators on the other end of the pull request, but it also makes it difficult to
determine how much scrutiny to apply to the contribution.
In a perfect world, AI assistance would produce equal or higher quality work than any human. That isn't the world we live in today, and in most cases
where human supervision or expertise is not in the loop, it's generating code that cannot be reasonably maintained or evolved.
### What we're looking for
When submitting AI-assisted contributions, please ensure they include:
- **Clear disclosure of AI use** - You are transparent about AI use and degree to which you're using it for the contribution
- **Human understanding and testing** - You've personally tested the changes and understand what they do
- **Clear rationale** - You can explain why the change is needed and how it fits within Spec Kit's goals
- **Concrete evidence** - Include test cases, scenarios, or examples that demonstrate the improvement
- **Your own analysis** - Share your thoughts on the end-to-end developer experience
### What we'll close
We reserve the right to close contributions that appear to be:
- Untested changes submitted without verification
- Generic suggestions that don't address specific Spec Kit needs
- Bulk submissions that show no human review or understanding
### Guidelines for success
The key is demonstrating that you understand and have validated your proposed changes. If a maintainer can easily tell that a contribution was generated entirely by AI without human input or testing, it likely needs more work before submission.
Contributors who consistently submit low-effort AI-generated changes may be restricted from further contributions at the maintainers' discretion.
Please be respectful to maintainers and disclose AI assistance.
## Resources
- [Spec-Driven Development Methodology](./spec-driven.md)

View File

@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

473
README.md
View File

@@ -1,36 +1,32 @@
<div align="center">
<img src="./media/logo_large.webp" alt="Spec Kit Logo" width="200" height="200"/>
<img src="./media/logo_small.webp"/>
<h1>🌱 Spec Kit</h1>
<h3><em>Build high-quality software faster.</em></h3>
</div>
<p align="center">
<strong>An open source toolkit that allows you to focus on product scenarios and predictable outcomes instead of vibe coding every piece from scratch.</strong>
<strong>An effort to allow organizations to focus on product scenarios rather than writing undifferentiated code with the help of Spec-Driven Development.</strong>
</p>
<p align="center">
<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/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>
</p>
[![Release](https://github.com/github/spec-kit/actions/workflows/release.yml/badge.svg)](https://github.com/github/spec-kit/actions/workflows/release.yml)
---
## Table of Contents
- [🤔 What is Spec-Driven Development?](#-what-is-spec-driven-development)
- [⚡ Get Started](#-get-started)
- [⚡ Get started](#-get-started)
- [📽️ Video Overview](#-video-overview)
- [🤖 Supported AI Agents](#-supported-ai-agents)
- [🔧 Specify CLI Reference](#-specify-cli-reference)
- [📚 Core Philosophy](#-core-philosophy)
- [🌟 Development Phases](#-development-phases)
- [🎯 Experimental Goals](#-experimental-goals)
- [<EFBFBD> APM Integration](#-apm-integration)
- [<EFBFBD>📚 Core philosophy](#-core-philosophy)
- [🌟 Development phases](#-development-phases)
- [🎯 Experimental goals](#-experimental-goals)
- [🔧 Prerequisites](#-prerequisites)
- [📖 Learn More](#-learn-more)
- [📋 Detailed Process](#-detailed-process)
- [📖 Learn more](#-learn-more)
- [📋 Detailed process](#-detailed-process)
- [🔍 Troubleshooting](#-troubleshooting)
- [👥 Maintainers](#-maintainers)
- [💬 Support](#-support)
- [🙏 Acknowledgements](#-acknowledgements)
- [📄 License](#-license)
@@ -39,97 +35,35 @@
Spec-Driven Development **flips the script** on traditional software development. For decades, code has been king — specifications were just scaffolding we built and discarded once the "real work" of coding began. Spec-Driven Development changes this: **specifications become executable**, directly generating working implementations rather than just guiding them.
## ⚡ Get Started
## ⚡ Get started
### 1. Install Specify CLI
### 1. Install Specify
Choose your preferred installation method:
#### Option 1: Persistent Installation (Recommended)
Install once and use everywhere:
```bash
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
```
Then use the tool directly:
```bash
# Create new project
specify init <PROJECT_NAME>
# Or initialize in existing project
specify init . --ai claude
# or
specify init --here --ai claude
# Check installed tools
specify check
```
To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed instructions. Quick upgrade:
```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
```
#### Option 2: One-time Usage
Run directly without installing:
Initialize your project depending on the coding agent you're using:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
```
**Benefits of persistent installation:**
### 2. Create the spec
- Tool stays installed and available in PATH
- No need to create shell aliases
- Better tool management with `uv tool list`, `uv tool upgrade`, `uv tool uninstall`
- Cleaner shell configuration
### 2. Establish project principles
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 **`/specify`** command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
```bash
/speckit.constitution Create principles focused on code quality, testing standards, user experience consistency, and performance requirements
/specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
```
### 3. Create the spec
### 3. Create a technical implementation plan
Use the **`/speckit.specify`** command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
Use the **`/plan`** command to provide your tech stack and architecture choices.
```bash
/speckit.specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
/plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
```
### 4. Create a technical implementation plan
### 4. Break down and implement
Use the **`/speckit.plan`** command to provide your tech stack and architecture choices.
```bash
/speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
```
### 5. Break down into tasks
Use **`/speckit.tasks`** to create an actionable task list from your implementation plan.
```bash
/speckit.tasks
```
### 6. Execute implementation
Use **`/speckit.implement`** to execute all tasks and build your feature according to the plan.
```bash
/speckit.implement
```
Use **`/tasks`** to create an actionable task list, then ask your agent to implement the feature.
For detailed step-by-step instructions, see our [comprehensive guide](./spec-driven.md).
@@ -139,58 +73,31 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
[![Spec Kit video header](/media/spec-kit-video-header.jpg)](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
## 🤖 Supported AI Agents
| Agent | Support | Notes |
| ------------------------------------------------------------------------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| [Qoder CLI](https://qoder.com/cli) | ✅ | |
| [Amazon Q Developer CLI](https://aws.amazon.com/developer/learning/q-developer-cli/) | ⚠️ | Amazon Q Developer CLI [does not support](https://github.com/aws/amazon-q-developer-cli/issues/3064) custom arguments for slash commands. |
| [Amp](https://ampcode.com/) | ✅ | |
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | ✅ | |
| [Claude Code](https://www.anthropic.com/claude-code) | ✅ | |
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | ✅ | |
| [Codex CLI](https://github.com/openai/codex) | ✅ | |
| [Cursor](https://cursor.sh/) | ✅ | |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | ✅ | |
| [GitHub Copilot](https://code.visualstudio.com/) | ✅ | |
| [IBM Bob](https://www.ibm.com/products/bob) | ✅ | IDE-based agent with slash command support |
| [Jules](https://jules.google.com/) | ✅ | |
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | ✅ | |
| [opencode](https://opencode.ai/) | ✅ | |
| [Qwen Code](https://github.com/QwenLM/qwen-code) | ✅ | |
| [Roo Code](https://roocode.com/) | ✅ | |
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | ✅ | |
| [Windsurf](https://windsurf.com/) | ✅ | |
| [Antigravity (agy)](https://agy.ai/) | ✅ | |
| Generic | ✅ | Bring your own agent — use `--ai generic --ai-commands-dir <path>` for unsupported agents |
## 🔧 Specify CLI Reference
The `specify` command supports the following options:
### Commands
| Command | Description |
| ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`, `windsurf`, `qwen`, `opencode`, `codex`, `shai`, `qodercli`) |
| Command | Description |
|-------------|----------------------------------------------------------------|
| `init` | Initialize a new Specify project from the latest template |
| `check` | Check for installed tools (`git`, `claude`, `gemini`, `code`/`code-insiders`, `cursor-agent`) |
| `apm` | APM - Agent Package Manager commands for Context management |
### `specify init` Arguments & Options
| Argument/Option | Type | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`, or use `.` for current directory) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, `cursor-agent`, `qwen`, `opencode`, `codex`, `windsurf`, `kilocode`, `auggie`, `roo`, `codebuddy`, `amp`, `shai`, `q`, `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/`) |
| `--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 |
| `--no-git` | Flag | Skip git repository initialization |
| `--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) |
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
| `--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`) |
| Argument/Option | Type | Description |
|------------------------|----------|------------------------------------------------------------------------------|
| `<project-name>` | Argument | Name for your new project directory (optional if using `--here`) |
| `--ai` | Option | AI assistant to use: `claude`, `gemini`, `copilot`, or `cursor` |
| `--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 |
| `--no-git` | Flag | Skip git repository initialization |
| `--here` | Flag | Initialize project in the current directory instead of creating a new one |
| `--skip-tls` | Flag | Skip SSL/TLS verification (not recommended) |
| `--debug` | Flag | Enable detailed debug output for troubleshooting |
| `--use-apm` | Flag | Include APM (Agent Package Manager) structure for context management |
### Examples
@@ -201,39 +108,17 @@ specify init my-project
# Initialize with specific AI assistant
specify init my-project --ai claude
# Initialize with APM support
specify init my-project --ai claude --use-apm
# Initialize with Cursor support
specify init my-project --ai cursor-agent
# Initialize with Qoder support
specify init my-project --ai qodercli
# Initialize with Windsurf support
specify init my-project --ai windsurf
# Initialize with Amp support
specify init my-project --ai amp
# Initialize with SHAI support
specify init my-project --ai shai
# Initialize with IBM Bob support
specify init my-project --ai bob
# 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 cursor
# Initialize with PowerShell scripts (Windows/cross-platform)
specify init my-project --ai copilot --script ps
# Initialize in current directory
specify init . --ai copilot
# or use the --here flag
specify init --here --ai copilot
# Force merge into current (non-empty) directory without confirmation
specify init . --force --ai copilot
# or
specify init --here --force --ai copilot
# Initialize in current directory with APM
specify init --here --ai copilot --use-apm
# Skip git initialization
specify init my-project --ai gemini --no-git
@@ -241,69 +126,70 @@ specify init my-project --ai gemini --no-git
# Enable debug output for troubleshooting
specify init my-project --ai claude --debug
# Use GitHub token for API requests (helpful for corporate environments)
specify init my-project --ai claude --github-token ghp_your_token_here
# Install agent skills with the project
specify init my-project --ai claude --ai-skills
# Initialize in current directory with agent skills
specify init --here --ai gemini --ai-skills
# Check system requirements
specify check
```
### Available Slash Commands
## 📦 APM Integration - NPM for Agent Context
After running `specify init`, your AI coding agent will have access to these slash commands for structured development:
**Context as Code Packages**: Package and share agent intelligence like npm packages. With APM, your agents get:
#### Core Commands
- **Team knowledge** from reusable context packages
- **Optimized context** through mathematical relevance scoring
- **Universal compatibility** via dynamically generated Agents.md files
Essential commands for the Spec-Driven Development workflow:
[Complete Context Management Guide →](docs/context-management.md)
| Command | Description |
| ----------------------- | ------------------------------------------------------------------------ |
| `/speckit.constitution` | Create or update project governing principles and development guidelines |
| `/speckit.specify` | Define what you want to build (requirements and user stories) |
| `/speckit.plan` | Create technical implementation plans with your chosen tech stack |
| `/speckit.tasks` | Generate actionable task lists for implementation |
| `/speckit.implement` | Execute all tasks to build the feature according to the plan |
Spec Kit includes full APM (Agent Package Manager) functionality for managing modular context packages and files:
#### Optional Commands
### Unified Initialization
```bash
# The --use-apm flag creates both SDD and APM structures
specify init my-project --ai claude --use-apm
```
Additional commands for enhanced quality and validation:
### APM Commands
```bash
# Core APM commands available under 'apm' subcommand
| Command | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `/speckit.clarify` | Clarify underspecified areas (recommended before `/speckit.plan`; formerly `/quizme`) |
| `/speckit.analyze` | Cross-artifact consistency & coverage analysis (run after `/speckit.tasks`, before `/speckit.implement`) |
| `/speckit.checklist` | Generate custom quality checklists that validate requirements completeness, clarity, and consistency (like "unit tests for English") |
# Install APM packages from apm.yml
specify apm install
### Environment Variables
# Add APM package to apm.yml and install
specify apm install org/repo
| Variable | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |
# Remove package from apm.yml and apm_modules
specify apm uninstall org/repo
## 📚 Core Philosophy
# Remove orphaned packages not in apm.yml
specify apm prune
# List installed APM packages
specify apm deps list
# Generate nested optimal AGENTS.md tree
# Uses installed APM packages and local context files
specify apm compile
```
## <20>📚 Core philosophy
Spec-Driven Development is a structured process that emphasizes:
- **Intent-driven development** where specifications define the "*what*" before the "*how*"
- **Intent-driven development** where specifications define the "_what_" before the "_how_"
- **Rich specification creation** using guardrails and organizational principles
- **Multi-step refinement** rather than one-shot code generation from prompts
- **Heavy reliance** on advanced AI model capabilities for specification interpretation
## 🌟 Development Phases
## 🌟 Development phases
| Phase | Focus | Key Activities |
| ---------------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **0-to-1 Development** ("Greenfield") | Generate from scratch | <ul><li>Start with high-level requirements</li><li>Generate specifications</li><li>Plan implementation steps</li><li>Build production-ready applications</li></ul> |
| **Creative Exploration** | Parallel implementations | <ul><li>Explore diverse solutions</li><li>Support multiple technology stacks & architectures</li><li>Experiment with UX patterns</li></ul> |
| **Iterative Enhancement** ("Brownfield") | Brownfield modernization | <ul><li>Add features iteratively</li><li>Modernize legacy systems</li><li>Adapt processes</li></ul> |
| Phase | Focus | Key Activities |
|-------|-------|----------------|
| **0-to-1 Development** ("Greenfield") | Generate from scratch | <ul><li>Start with high-level requirements</li><li>Generate specifications</li><li>Plan implementation steps</li><li>Build production-ready applications</li></ul> |
| **Creative Exploration** | Parallel implementations | <ul><li>Explore diverse solutions</li><li>Support multiple technology stacks & architectures</li><li>Experiment with UX patterns</li></ul> |
| **Iterative Enhancement** ("Brownfield") | Brownfield modernization | <ul><li>Add features iteratively</li><li>Modernize legacy systems</li><li>Adapt processes</li></ul> |
## 🎯 Experimental Goals
## 🎯 Experimental goals
Our research and experimentation focus on:
@@ -331,22 +217,20 @@ Our research and experimentation focus on:
## 🔧 Prerequisites
- **Linux/macOS/Windows**
- [Supported](#-supported-ai-agents) AI coding agent.
- **Linux/macOS** (or WSL2 on Windows)
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Cursor](https://cursor.sh/)
- [uv](https://docs.astral.sh/uv/) for package management
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)
If you encounter issues with an agent, please open an issue so we can refine the integration.
## 📖 Learn More
## 📖 Learn more
- **[Complete Spec-Driven Development Methodology](./spec-driven.md)** - Deep dive into the full process
- **[Detailed Walkthrough](#-detailed-process)** - Step-by-step implementation guide
---
## 📋 Detailed Process
## 📋 Detailed process
<details>
<summary>Click to expand the detailed step-by-step walkthrough</summary>
@@ -360,13 +244,7 @@ specify init <project_name>
Or initialize in the current directory:
```bash
specify init .
# or use the --here flag
specify init --here
# Skip confirmation when the directory already has files
specify init . --force
# or
specify init --here --force
```
![Specify CLI bootstrapping a new project in the terminal](./media/specify_cli.gif)
@@ -377,50 +255,28 @@ You will be prompted to select the AI agent you are using. You can also proactiv
specify init <project_name> --ai claude
specify init <project_name> --ai gemini
specify init <project_name> --ai copilot
# Or in current directory:
specify init . --ai claude
specify init . --ai codex
# or use --here flag
specify init --here --ai claude
specify init --here --ai codex
# Force merge into a non-empty current directory
specify init . --force --ai claude
# or
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, or Amazon Q Developer 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:
The CLI will check if you have Claude Code or Gemini 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
specify init <project_name> --ai claude --ignore-agent-tools
```
### **STEP 1:** Establish project principles
### **STEP 1:** Bootstrap the project
Go to the project folder and run your AI agent. In our example, we're using `claude`.
![Bootstrapping Claude Code environment](./media/bootstrap-claude-code.gif)
You will know that things are configured correctly if you see the `/speckit.constitution`, `/speckit.specify`, `/speckit.plan`, `/speckit.tasks`, and `/speckit.implement` commands available.
You will know that things are configured correctly if you see the `/specify`, `/plan`, and `/tasks` commands available.
The first step should be establishing your project's governing principles using the `/speckit.constitution` command. This helps ensure consistent decision-making throughout all subsequent development phases:
The first step should be creating a new project scaffolding. Use `/specify` command and then provide the concrete requirements for the project you want to develop.
```text
/speckit.constitution Create principles focused on code quality, testing standards, user experience consistency, and performance requirements. Include governance for how these principles should guide technical decisions and implementation choices.
```
This step creates or updates the `.specify/memory/constitution.md` file with your project's foundational guidelines that the AI agent will reference during specification, planning, and implementation phases.
### **STEP 2:** Create project specifications
With your project principles established, you can now create the functional specifications. Use the `/speckit.specify` command and then provide the concrete requirements for the project you want to develop.
> [!IMPORTANT]
> Be as explicit as possible about *what* you are trying to build and *why*. **Do not focus on the tech stack at this point**.
>[!IMPORTANT]
>Be as explicit as possible about _what_ you are trying to build and _why_. **Do not focus on the tech stack at this point**.
An example prompt:
@@ -452,38 +308,28 @@ The produced specification should contain a set of user stories and functional r
At this stage, your project folder contents should resemble the following:
```text
── .specify
├── memory
└── constitution.md
├── scripts
├── check-prerequisites.sh
├── common.sh
├── create-new-feature.sh
│ ├── setup-plan.sh
│ └── update-claude-md.sh
├── specs
│ └── 001-create-taskify
│ └── spec.md
└── templates
├── plan-template.md
├── spec-template.md
└── tasks-template.md
── memory
├── constitution.md
└── constitution_update_checklist.md
├── scripts
├── check-task-prerequisites.sh
├── common.sh
├── create-new-feature.sh
│ ├── get-feature-paths.sh
│ ├── setup-plan.sh
│ └── update-claude-md.sh
├── specs
│ └── 001-create-taskify
└── spec.md
└── templates
├── plan-template.md
├── spec-template.md
└── tasks-template.md
```
### **STEP 3:** Functional specification clarification (required before planning)
### **STEP 2:** Functional specification clarification
With the baseline specification created, you can go ahead and clarify any of the requirements that were not captured properly within the first shot attempt.
You should run the structured clarification workflow **before** creating a technical plan to reduce rework downstream.
Preferred order:
1. Use `/speckit.clarify` (structured) sequential, coverage-based questioning that records answers in a Clarifications section.
2. Optionally follow up with ad-hoc free-form refinement if something still feels vague.
If you intentionally want to skip clarification (e.g., spike or exploratory prototype), explicitly state that so the agent doesn't block on missing clarifications.
Example free-form refinement prompt (after `/speckit.clarify` if still needed):
With the baseline specification created, you can go ahead and clarify any of the requirements that were not captured properly within the first shot attempt. For example, you could use a prompt like this within the same Claude Code session:
```text
For each sample project or project that you create there should be a variable number of tasks between 5 and 15
@@ -499,9 +345,9 @@ Read the review and acceptance checklist, and check off each item in the checkli
It's important to use the interaction with Claude Code as an opportunity to clarify and ask questions around the specification - **do not treat its first attempt as final**.
### **STEP 4:** Generate a plan
### **STEP 3:** Generate a plan
You can now be specific about the tech stack and other technical requirements. You can use the `/speckit.plan` command that is built into the project template with a prompt like this:
You can now be specific about the tech stack and other technical requirements. You can use the `/plan` command that is built into the project template with a prompt like this:
```text
We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use
@@ -515,23 +361,25 @@ The output of this step will include a number of implementation detail documents
.
├── CLAUDE.md
├── memory
── constitution.md
── constitution.md
│ └── constitution_update_checklist.md
├── scripts
├── check-prerequisites.sh
├── common.sh
├── create-new-feature.sh
├── setup-plan.sh
└── update-claude-md.sh
├── check-task-prerequisites.sh
├── common.sh
├── create-new-feature.sh
├── get-feature-paths.sh
├── setup-plan.sh
│ └── update-claude-md.sh
├── specs
└── 001-create-taskify
├── contracts
├── api-spec.json
└── signalr-spec.md
├── data-model.md
├── plan.md
├── quickstart.md
├── research.md
└── spec.md
└── 001-create-taskify
├── contracts
├── api-spec.json
└── signalr-spec.md
├── data-model.md
├── plan.md
├── quickstart.md
├── research.md
└── spec.md
└── templates
├── CLAUDE-template.md
├── plan-template.md
@@ -563,10 +411,10 @@ researching .NET Aspire in general and I don't think that's gonna do much for us
That's way too untargeted research. The research needs to help you solve a specific targeted question.
```
> [!NOTE]
> Claude Code might be over-eager and add components that you did not ask for. Ask it to clarify the rationale and the source of the change.
>[!NOTE]
>Claude Code might be over-eager and add components that you did not ask for. Ask it to clarify the rationale and the source of the change.
### **STEP 5:** Have Claude Code validate the plan
### **STEP 4:** Have Claude Code validate the plan
With the plan in place, you should have Claude Code run through it to make sure that there are no missing pieces. You can use a prompt like this:
@@ -582,48 +430,23 @@ This helps refine the implementation plan and helps you avoid potential blind sp
You can also ask Claude Code (if you have the [GitHub CLI](https://docs.github.com/en/github-cli/github-cli) installed) to go ahead and create a pull request from your current branch to `main` with a detailed description, to make sure that the effort is properly tracked.
> [!NOTE]
> Before you have the agent implement it, it's also worth prompting Claude Code to cross-check the details to see if there are any over-engineered pieces (remember - it can be over-eager). If over-engineered components or decisions exist, you can ask Claude Code to resolve them. Ensure that Claude Code follows the [constitution](base/memory/constitution.md) as the foundational piece that it must adhere to when establishing the plan.
>[!NOTE]
>Before you have the agent implement it, it's also worth prompting Claude Code to cross-check the details to see if there are any over-engineered pieces (remember - it can be over-eager). If over-engineered components or decisions exist, you can ask Claude Code to resolve them. Ensure that Claude Code follows the [constitution](base/memory/constitution.md) as the foundational piece that it must adhere to when establishing the plan.
### **STEP 6:** Generate task breakdown with /speckit.tasks
### STEP 5: Implementation
With the implementation plan validated, you can now break down the plan into specific, actionable tasks that can be executed in the correct order. Use the `/speckit.tasks` command to automatically generate a detailed task breakdown from your implementation plan:
Once ready, instruct Claude Code to implement your solution (example path included):
```text
/speckit.tasks
implement specs/002-create-taskify/plan.md
```
This step creates a `tasks.md` file in your feature specification directory that contains:
Claude Code will spring into action and will start creating the implementation.
- **Task breakdown organized by user story** - Each user story becomes a separate implementation phase with its own set of tasks
- **Dependency management** - Tasks are ordered to respect dependencies between components (e.g., models before services, services before endpoints)
- **Parallel execution markers** - Tasks that can run in parallel are marked with `[P]` to optimize development workflow
- **File path specifications** - Each task includes the exact file paths where implementation should occur
- **Test-driven development structure** - If tests are requested, test tasks are included and ordered to be written before implementation
- **Checkpoint validation** - Each user story phase includes checkpoints to validate independent functionality
>[!IMPORTANT]
>Claude Code will execute local CLI commands (such as `dotnet`) - make sure you have them installed on your machine.
The generated tasks.md provides a clear roadmap for the `/speckit.implement` command, ensuring systematic implementation that maintains code quality and allows for incremental delivery of user stories.
### **STEP 7:** Implementation
Once ready, use the `/speckit.implement` command to execute your implementation plan:
```text
/speckit.implement
```
The `/speckit.implement` command will:
- Validate that all prerequisites are in place (constitution, spec, plan, and tasks)
- Parse the task breakdown from `tasks.md`
- Execute tasks in the correct order, respecting dependencies and parallel execution markers
- Follow the TDD approach defined in your task plan
- Provide progress updates and handle errors appropriately
> [!IMPORTANT]
> The AI agent will execute local CLI commands (such as `dotnet`, `npm`, etc.) - make sure you have the required tools installed on your machine.
Once the implementation is complete, test the application and resolve any runtime errors that may not be visible in CLI logs (e.g., browser console errors). You can copy and paste such errors back to your AI agent for resolution.
Once the implementation step is done, ask Claude Code to try to run the application and resolve any emerging build errors. If the application runs, but there are _runtime errors_ that are not directly available to Claude Code through CLI logs (e.g., errors rendered in browser logs), copy and paste the error in Claude Code and have it attempt to resolve it.
</details>
@@ -648,6 +471,12 @@ echo "Cleaning up..."
rm gcm-linux_amd64.2.6.1.deb
```
## 👥 Maintainers
- Den Delimarsky ([@localden](https://github.com/localden))
- John Lam ([@jflam](https://github.com/jflam))
- Daniel Meppiel [@danielmeppiel](https://github.com/danielmeppiel)
## 💬 Support
For support, please open a [GitHub issue](https://github.com/github/spec-kit/issues/new). We welcome bug reports, feature requests, and questions about using Spec-Driven Development.

View File

@@ -1,10 +1,10 @@
# Security Policy
Thanks for helping make GitHub safe for everyone.
# Security
GitHub takes the security of our software products and services seriously, including all of the open source code repositories managed through our GitHub organizations, such as [GitHub](https://github.com/GitHub).
Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation.
Even though [open source repositories are outside of the scope of our bug bounty program](https://bounty.github.com/index.html#scope) and therefore not eligible for bounty rewards, we will ensure that your finding gets passed along to the appropriate maintainers for remediation.
## Reporting Security Issues
@@ -16,16 +16,16 @@ Instead, please send an email to opensource-security[@]github.com.
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
- The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
* The type of issue (e.g., buffer overflow, SQL injection, or cross-site scripting)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Policy
See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms)
See [GitHub's Safe Harbor Policy](https://docs.github.com/en/site-policy/security-policies/github-bug-bounty-program-legal-safe-harbor#1-safe-harbor-terms)

View File

@@ -1,4 +1,4 @@
# Support
# Support
## How to file issues and get help

1
docs/.gitignore vendored
View File

@@ -6,4 +6,3 @@ obj/
# Temporary files
*.tmp
*.log

View File

@@ -7,13 +7,11 @@ This folder contains the documentation source files for Spec Kit, built using [D
To build the documentation locally:
1. Install DocFX:
```bash
dotnet tool install -g docfx
```
2. Build the documentation:
```bash
cd docs
docfx docfx.json --serve

View File

@@ -0,0 +1,59 @@
# Context Management with APM
## NPM for Agent Context
Just like npm revolutionized JavaScript by enabling package reuse, APM creates an ecosystem for sharing agent context.
## Package Composition & Reuse
```yaml
# Your project inherits team knowledge via apm.yml file in the root
dependencies:
apm:
- company/design-system # UI patterns, brand guidelines
- company/security-standards # Auth patterns, data handling
- community/best-practices # Industry standards
```
**Result**: Your project gets all the instructions of above packages applied via dynamically generated Agents.md files using `specify apm compile`. These files are optimally generated to minimize contextual load for Agents compatible with the Agents.md standard.
**Enterprise Scenario**: Design team creates accessibility guidelines once → entire organization uses them → agents work consistently across all projects.
## Mathematical Context Optimization
**The Technical Foundation**: APM uses mathematical optimization to solve the context efficiency problem.
```
Context_Efficiency = Relevant_Instructions / Total_Instructions_Loaded
```
**Why This Matters**: When agents work in `/styles/` directory, they shouldn't load Python compliance rules. APM's Context Optimization Engine ensures agents get minimal, highly relevant context.
**The Algorithm**: Constraint satisfaction optimization that finds placement minimizing context pollution while maximizing relevance. Each instruction gets mathematically optimal placement across the project hierarchy.
## Quick Start
```bash
specify init my-project --use-apm --ai copilot
specify apm install company/design-system
specify apm compile # Mathematical optimization generates distributed AGENTS.md files
```
## Universal Agent Compatibility
APM generates distributed `AGENTS.md` files compatible with the [agents.md standard](https://agents.md), working with any coding agent (GitHub Copilot, Cursor, Claude, Codex, Aider, etc.).
## Authentication Setup (Optional)
```bash
export GITHUB_APM_PAT=your_fine_grained_token_here
```
Only needed for private packages. Public community packages work without authentication.
## The Complete Value
1. **Package Ecosystem** - Share and compose agent intelligence like code dependencies
2. **Mathematical Optimization** - Context Optimization Engine ensures relevance without pollution
3. **Universal Standards** - Works with any agent via industry-standard agents.md format
4. **Enterprise Ready** - Team knowledge scales across entire organizations

View File

@@ -68,4 +68,3 @@
}
}
}

View File

@@ -12,14 +12,13 @@ Spec-Driven Development **flips the script** on traditional software development
- [Installation Guide](installation.md)
- [Quick Start Guide](quickstart.md)
- [Upgrade Guide](upgrade.md)
- [Local Development](local-development.md)
## Core Philosophy
Spec-Driven Development is a structured process that emphasizes:
- **Intent-driven development** where specifications define the "*what*" before the "*how*"
- **Intent-driven development** where specifications define the "_what_" before the "_how_"
- **Rich specification creation** using guardrails and organizational principles
- **Multi-step refinement** rather than one-shot code generation from prompts
- **Heavy reliance** on advanced AI model capabilities for specification interpretation
@@ -37,31 +36,27 @@ Spec-Driven Development is a structured process that emphasizes:
Our research and experimentation focus on:
### Technology Independence
- Create applications using diverse technology stacks
- Validate the hypothesis that Spec-Driven Development is a process not tied to specific technologies, programming languages, or frameworks
### Enterprise Constraints
- Demonstrate mission-critical application development
- Incorporate organizational constraints (cloud providers, tech stacks, engineering practices)
- Support enterprise design systems and compliance requirements
### User-Centric Development
- Build applications for different user cohorts and preferences
- Support various development approaches (from vibe-coding to AI-native development)
### Creative & Iterative Processes
- Validate the concept of parallel implementation exploration
- Provide robust iterative feature development workflows
- Extend processes to handle upgrades and modernization tasks
## Contributing
Please see our [Contributing Guide](https://github.com/github/spec-kit/blob/main/CONTRIBUTING.md) for information on how to contribute to this project.
Please see our [Contributing Guide](CONTRIBUTING.md) for information on how to contribute to this project.
## Support
For support, please check our [Support Guide](https://github.com/github/spec-kit/blob/main/SUPPORT.md) or open an issue on GitHub.
For support, please check our [Support Guide](SUPPORT.md) or open an issue on GitHub.

View File

@@ -3,7 +3,7 @@
## Prerequisites
- **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) or [Gemini CLI](https://github.com/google-gemini/gemini-cli)
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), or [Gemini CLI](https://github.com/google-gemini/gemini-cli)
- [uv](https://docs.astral.sh/uv/) for package management
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)
@@ -21,8 +21,6 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
Or initialize in the current directory:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init .
# or use the --here flag
uvx --from git+https://github.com/github/spec-kit.git specify init --here
```
@@ -34,7 +32,6 @@ You can proactively specify your AI agent during initialization:
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 specify init <project_name> --ai gemini
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai copilot
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --ai codebuddy
```
### Specify Script Type (Shell vs PowerShell)
@@ -42,13 +39,11 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <project_name
All automation scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants.
Auto behavior:
- Windows default: `ps`
- Other OS default: `sh`
- Interactive mode: you'll be prompted unless you pass `--script`
Force a specific script type:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script sh
uvx --from git+https://github.com/github/spec-kit.git specify init <project_name> --script ps
@@ -65,10 +60,9 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <project_name
## Verification
After initialization, you should see the following commands available in your AI agent:
- `/speckit.specify` - Create specifications
- `/speckit.plan` - Generate implementation plans
- `/speckit.tasks` - Break down into actionable tasks
- `/specify` - Create specifications
- `/plan` - Generate implementation plans
- `/tasks` - Break down into actionable tasks
The `.specify/scripts` directory will contain both `.sh` and `.ps1` scripts.

View File

@@ -73,14 +73,12 @@ uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --ai copilot --igno
```
Set an environment variable for convenience:
```bash
export SPEC_KIT_SRC=/mnt/c/GitHub/spec-kit
uvx --from "$SPEC_KIT_SRC" specify init demo-env --ai copilot --ignore-agent-tools --script ps
```
(Optional) Define a shell function:
```bash
specify-dev() { uvx --from /mnt/c/GitHub/spec-kit specify "$@"; }
# Then
@@ -95,13 +93,11 @@ After running an `init`, check that shell scripts are executable on POSIX system
ls -l scripts | grep .sh
# Expect owner execute bit (e.g. -rwxr-xr-x)
```
On Windows you will instead use the `.ps1` scripts (no chmod needed).
## 6. Run Lint / Basic Checks (Add Your Own)
Currently no enforced lint config is bundled, but you can quickly sanity check importability:
```bash
python -c "import specify_cli; print('Import OK')"
```
@@ -114,7 +110,6 @@ Validate packaging before publishing:
uv build
ls dist/
```
Install the built artifact into a fresh throwaway environment if needed.
## 8. Using a Temporary Workspace
@@ -125,7 +120,6 @@ When testing `init --here` in a dirty directory, create a temp workspace:
mkdir /tmp/spec-test && cd /tmp/spec-test
python -m src.specify_cli init --here --ai claude --ignore-agent-tools --script sh # if repo copied here
```
Or copy only the modified CLI portion if you want a lighter sandbox.
## 9. Debug Network / TLS Skips
@@ -136,7 +130,6 @@ If you need to bypass TLS validation while experimenting:
specify check --skip-tls
specify init demo --skip-tls --ai gemini --ignore-agent-tools --script ps
```
(Use only for local experimentation.)
## 10. Rapid Edit Loop Summary
@@ -153,7 +146,6 @@ specify init demo --skip-tls --ai gemini --ignore-agent-tools --script ps
## 11. Cleaning Up
Remove build artifacts / virtual env quickly:
```bash
rm -rf .venv dist build *.egg-info
```
@@ -173,3 +165,4 @@ rm -rf .venv dist build *.egg-info
- Update docs and run through Quick Start using your modified CLI
- Open a PR when satisfied
- (Optional) Tag a release once changes land in `main`

View File

@@ -2,101 +2,49 @@
This guide will help you get started with Spec-Driven Development using Spec Kit.
> [!NOTE]
> All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
> NEW: All automation scripts now provide both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on OS unless you pass `--script sh|ps`.
## The 6-Step Process
## The 4-Step Process
> [!TIP]
> **Context Awareness**: Spec Kit commands automatically detect the active feature based on your current Git branch (e.g., `001-feature-name`). To switch between different specifications, simply switch Git branches.
### 1. Install Specify
### Step 1: Install Specify
**In your terminal**, run the `specify` CLI command to initialize your project:
Initialize your project depending on the coding agent you're using:
```bash
# Create a new project directory
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
# OR initialize in the current directory
uvx --from git+https://github.com/github/spec-kit.git specify init .
```
Pick script type explicitly (optional):
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script ps # Force PowerShell
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME> --script sh # Force POSIX shell
```
### Step 2: Define Your Constitution
### 2. Create the Spec
**In your AI Agent's chat interface**, use the `/speckit.constitution` slash command to establish the core rules and principles for your project. You should provide your project's specific principles as arguments.
```markdown
/speckit.constitution This project follows a "Library-First" approach. All features must be implemented as standalone libraries first. We use TDD strictly. We prefer functional programming patterns.
```
### Step 3: Create the Spec
**In the chat**, use the `/speckit.specify` slash command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
```markdown
/speckit.specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
```
### Step 4: Refine the Spec
**In the chat**, use the `/speckit.clarify` slash command to identify and resolve ambiguities in your specification. You can provide specific focus areas as arguments.
Use the `/specify` command to describe what you want to build. Focus on the **what** and **why**, not the tech stack.
```bash
/speckit.clarify Focus on security and performance requirements.
/specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
```
### Step 5: Create a Technical Implementation Plan
### 3. Create a Technical Implementation Plan
**In the chat**, use the `/speckit.plan` slash command to provide your tech stack and architecture choices.
Use the `/plan` command to provide your tech stack and architecture choices.
```markdown
/speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
```bash
/plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
```
### Step 6: Break Down and Implement
### 4. Break Down and Implement
**In the chat**, use the `/speckit.tasks` slash command to create an actionable task list.
```markdown
/speckit.tasks
```
Optionally, validate the plan with `/speckit.analyze`:
```markdown
/speckit.analyze
```
Then, use the `/speckit.implement` slash command to execute the plan.
```markdown
/speckit.implement
```
> [!TIP]
> **Phased Implementation**: For complex projects, implement in phases to avoid overwhelming the agent's context. Start with core functionality, validate it works, then add features incrementally.
Use `/tasks` to create an actionable task list, then ask your agent to implement the feature.
## Detailed Example: Building Taskify
Here's a complete example of building a team productivity platform:
### Step 1: Define Constitution
Initialize the project's constitution to set ground rules:
```markdown
/speckit.constitution Taskify is a "Security-First" application. All user inputs must be validated. We use a microservices architecture. Code must be fully documented.
```
### Step 2: Define Requirements with `/speckit.specify`
### Step 1: Define Requirements with `/specify`
```text
Develop Taskify, a team productivity platform. It should allow users to create projects, add team members,
@@ -105,64 +53,60 @@ let's call it "Create Taskify," let's have multiple users but the users will be
I want five users in two different categories, one product manager and four engineers. Let's create three
different sample projects. Let's have the standard Kanban columns for the status of each task, such as "To Do,"
"In Progress," "In Review," and "Done." There will be no login for this application as this is just the very
first testing thing to ensure that our basic features are set up.
first testing thing to ensure that our basic features are set up. For each task in the UI for a task card,
you should be able to change the current status of the task between the different columns in the Kanban work board.
You should be able to leave an unlimited number of comments for a particular card. You should be able to, from that task
card, assign one of the valid users. When you first launch Taskify, it's going to give you a list of the five users to pick
from. There will be no password required. When you click on a user, you go into the main view, which displays the list of
projects. When you click on a project, you open the Kanban board for that project. You're going to see the columns.
You'll be able to drag and drop cards back and forth between different columns. You will see any cards that are
assigned to you, the currently logged in user, in a different color from all the other ones, so you can quickly
see yours. You can edit any comments that you make, but you can't edit comments that other people made. You can
delete any comments that you made, but you can't delete comments anybody else made.
```
### Step 3: Refine the Specification
### Step 2: Refine the Specification
Use the `/speckit.clarify` command to interactively resolve any ambiguities in your specification. You can also provide specific details you want to ensure are included.
After the initial specification is created, clarify any missing requirements:
```bash
/speckit.clarify I want to clarify the task card details. For each task in the UI for a task card, you should be able to change the current status of the task between the different columns in the Kanban work board. You should be able to leave an unlimited number of comments for a particular card. You should be able to, from that task card, assign one of the valid users.
```text
For each sample project or project that you create there should be a variable number of tasks between 5 and 15
tasks for each one randomly distributed into different states of completion. Make sure that there's at least
one task in each stage of completion.
```
You can continue to refine the spec with more details using `/speckit.clarify`:
Also validate the specification checklist:
```bash
/speckit.clarify When you first launch Taskify, it's going to give you a list of the five users to pick from. There will be no password required. When you click on a user, you go into the main view, which displays the list of projects. When you click on a project, you open the Kanban board for that project. You're going to see the columns. You'll be able to drag and drop cards back and forth between different columns. You will see any cards that are assigned to you, the currently logged in user, in a different color from all the other ones, so you can quickly see yours. You can edit any comments that you make, but you can't edit comments that other people made. You can delete any comments that you made, but you can't delete comments anybody else made.
```text
Read the review and acceptance checklist, and check off each item in the checklist if the feature spec meets the criteria. Leave it empty if it does not.
```
### Step 4: Validate the Spec
Validate the specification checklist using the `/speckit.checklist` command:
```bash
/speckit.checklist
```
### Step 5: Generate Technical Plan with `/speckit.plan`
### Step 3: Generate Technical Plan with `/plan`
Be specific about your tech stack and technical requirements:
```bash
/speckit.plan We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API, tasks API, and a notifications API.
```text
We are going to generate this using .NET Aspire, using Postgres as the database. The frontend should use
Blazor server with drag-and-drop task boards, real-time updates. There should be a REST API created with a projects API,
tasks API, and a notifications API.
```
### Step 6: Define Tasks
### Step 4: Validate and Implement
Generate an actionable task list using the `/speckit.tasks` command:
Have your AI agent audit the implementation plan:
```bash
/speckit.tasks
```
### Step 7: Validate and Implement
Have your AI agent audit the implementation plan using `/speckit.analyze`:
```bash
/speckit.analyze
```text
Now I want you to go and audit the implementation plan and the implementation detail files.
Read through it with an eye on determining whether or not there is a sequence of tasks that you need
to be doing that are obvious from reading this. Because I don't know if there's enough here.
```
Finally, implement the solution:
```bash
/speckit.implement
```text
implement specs/002-create-taskify/plan.md
```
> [!TIP]
> **Phased Implementation**: For large projects like Taskify, consider implementing in phases (e.g., Phase 1: Basic project/task structure, Phase 2: Kanban functionality, Phase 3: Comments and assignments). This prevents context saturation and allows for validation at each stage.
## Key Principles
- **Be explicit** about what you're building and why
@@ -173,6 +117,6 @@ Finally, implement the solution:
## Next Steps
- Read the [complete methodology](../spec-driven.md) for in-depth guidance
- Check out [more examples](../templates) in the repository
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
- Read the complete methodology for in-depth guidance
- Check out more examples in the repository
- Explore the source code on GitHub

View File

@@ -9,8 +9,6 @@
href: installation.md
- name: Quick Start
href: quickstart.md
- name: Upgrade
href: upgrade.md
# Development workflows
- name: Development

View File

@@ -1,444 +0,0 @@
# Upgrade Guide
> You have Spec Kit installed and want to upgrade to the latest version to get new features, bug fixes, or updated slash commands. This guide covers both upgrading the CLI tool and updating your project files.
---
## Quick Reference
| 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` | 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 |
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
---
## Part 1: Upgrade the CLI Tool
The CLI tool (`specify`) is separate from your project files. Upgrade it to get the latest features and bug fixes.
### If you installed with `uv tool install`
```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
```
### If you use one-shot `uvx` commands
No upgrade needed—`uvx` always fetches the latest version. Just run your commands as normal:
```bash
uvx --from git+https://github.com/github/spec-kit.git specify init --here --ai copilot
```
### Verify the upgrade
```bash
specify check
```
This shows installed tools and confirms the CLI is working.
---
## Part 2: Updating Project Files
When Spec Kit releases new features (like new slash commands or updated templates), you need to refresh your project's Spec Kit files.
### What gets updated?
Running `specify init --here --force` will update:
-**Slash command files** (`.claude/commands/`, `.github/prompts/`, etc.)
-**Script files** (`.specify/scripts/`)
-**Template files** (`.specify/templates/`)
-**Shared memory files** (`.specify/memory/`) - **⚠️ See warnings below**
### What stays safe?
These files are **never touched** by the upgrade—the template packages don't even contain them:
-**Your specifications** (`specs/001-my-feature/spec.md`, etc.) - **CONFIRMED SAFE**
-**Your implementation plans** (`specs/001-my-feature/plan.md`, `tasks.md`, etc.) - **CONFIRMED SAFE**
-**Your source code** - **CONFIRMED SAFE**
-**Your git history** - **CONFIRMED SAFE**
The `specs/` directory is completely excluded from template packages and will never be modified during upgrades.
### Update command
Run this inside your project directory:
```bash
specify init --here --force --ai <your-agent>
```
Replace `<your-agent>` with your AI assistant. Refer to this list of [Supported AI Agents](../README.md#-supported-ai-agents)
**Example:**
```bash
specify init --here --force --ai copilot
```
### Understanding the `--force` flag
Without `--force`, the CLI warns you and asks for confirmation:
```text
Warning: Current directory is not empty (25 items)
Template files will be merged with existing content and may overwrite existing files
Proceed? [y/N]
```
With `--force`, it skips the confirmation and proceeds immediately.
**Important: Your `specs/` directory is always safe.** The `--force` flag only affects template files (commands, scripts, templates, memory). Your feature specifications, plans, and tasks in `specs/` are never included in upgrade packages and cannot be overwritten.
---
## ⚠️ Important Warnings
### 1. Constitution file will be overwritten
**Known issue:** `specify init --here --force` currently overwrites `.specify/memory/constitution.md` with the default template, erasing any customizations you made.
**Workaround:**
```bash
# 1. Back up your constitution before upgrading
cp .specify/memory/constitution.md .specify/memory/constitution-backup.md
# 2. Run the upgrade
specify init --here --force --ai copilot
# 3. Restore your customized constitution
mv .specify/memory/constitution-backup.md .specify/memory/constitution.md
```
Or use git to restore it:
```bash
# After upgrade, restore from git history
git restore .specify/memory/constitution.md
```
### 2. Custom template modifications
If you customized any templates in `.specify/templates/`, the upgrade will overwrite them. Back them up first:
```bash
# Back up custom templates
cp -r .specify/templates .specify/templates-backup
# After upgrade, merge your changes back manually
```
### 3. Duplicate slash commands (IDE-based agents)
Some IDE-based agents (like Kilo Code, Windsurf) may show **duplicate slash commands** after upgrading—both old and new versions appear.
**Solution:** Manually delete the old command files from your agent's folder.
**Example for Kilo Code:**
```bash
# Navigate to the agent's commands folder
cd .kilocode/rules/
# List files and identify duplicates
ls -la
# Delete old versions (example filenames - yours may differ)
rm speckit.specify-old.md
rm speckit.plan-v1.md
```
Restart your IDE to refresh the command list.
---
## Common Scenarios
### Scenario 1: "I just want new slash commands"
```bash
# Upgrade CLI (if using persistent install)
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# Update project files to get new commands
specify init --here --force --ai copilot
# Restore your constitution if customized
git restore .specify/memory/constitution.md
```
### Scenario 2: "I customized templates and constitution"
```bash
# 1. Back up customizations
cp .specify/memory/constitution.md /tmp/constitution-backup.md
cp -r .specify/templates /tmp/templates-backup
# 2. Upgrade CLI
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# 3. Update project
specify init --here --force --ai copilot
# 4. Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
# Manually merge template changes if needed
```
### Scenario 3: "I see duplicate slash commands in my IDE"
This happens with IDE-based agents (Kilo Code, Windsurf, Roo Code, etc.).
```bash
# Find the agent folder (example: .kilocode/rules/)
cd .kilocode/rules/
# List all files
ls -la
# Delete old command files
rm speckit.old-command-name.md
# Restart your IDE
```
### Scenario 4: "I'm working on a project without Git"
If you initialized your project with `--no-git`, you can still upgrade:
```bash
# Manually back up files you customized
cp .specify/memory/constitution.md /tmp/constitution-backup.md
# Run upgrade
specify init --here --force --ai copilot --no-git
# Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
```
The `--no-git` flag skips git initialization but doesn't affect file updates.
---
## Using `--no-git` Flag
The `--no-git` flag tells Spec Kit to **skip git repository initialization**. This is useful when:
- You manage version control differently (Mercurial, SVN, etc.)
- Your project is part of a larger monorepo with existing git setup
- You're experimenting and don't want version control yet
**During initial setup:**
```bash
specify init my-project --ai copilot --no-git
```
**During upgrade:**
```bash
specify init --here --force --ai copilot --no-git
```
### What `--no-git` does NOT do
❌ Does NOT prevent file updates
❌ Does NOT skip slash command installation
❌ Does NOT affect template merging
It **only** skips running `git init` and creating the initial commit.
### Working without Git
If you use `--no-git`, you'll need to manage feature directories manually:
**Set the `SPECIFY_FEATURE` environment variable** before using planning commands:
```bash
# Bash/Zsh
export SPECIFY_FEATURE="001-my-feature"
# PowerShell
$env:SPECIFY_FEATURE = "001-my-feature"
```
This tells Spec Kit which feature directory to use when creating specs, plans, and tasks.
**Why this matters:** Without git, Spec Kit can't detect your current branch name to determine the active feature. The environment variable provides that context manually.
---
## Troubleshooting
### "Slash commands not showing up after upgrade"
**Cause:** Agent didn't reload the command files.
**Fix:**
1. **Restart your IDE/editor** completely (not just reload window)
2. **For CLI-based agents**, verify files exist:
```bash
ls -la .claude/commands/ # Claude Code
ls -la .gemini/commands/ # Gemini
ls -la .cursor/commands/ # Cursor
```
3. **Check agent-specific setup:**
- Codex requires `CODEX_HOME` environment variable
- Some agents need workspace restart or cache clearing
### "I lost my constitution customizations"
**Fix:** Restore from git or backup:
```bash
# If you committed before upgrading
git restore .specify/memory/constitution.md
# If you backed up manually
cp /tmp/constitution-backup.md .specify/memory/constitution.md
```
**Prevention:** Always commit or back up `constitution.md` before upgrading.
### "Warning: Current directory is not empty"
**Full warning message:**
```text
Warning: Current directory is not empty (25 items)
Template files will be merged with existing content and may overwrite existing files
Do you want to continue? [y/N]
```
**What this means:**
This warning appears when you run `specify init --here` (or `specify init .`) in a directory that already has files. It's telling you:
1. **The directory has existing content** - In the example, 25 files/folders
2. **Files will be merged** - New template files will be added alongside your existing files
3. **Some files may be overwritten** - If you already have Spec Kit files (`.claude/`, `.specify/`, etc.), they'll be replaced with the new versions
**What gets overwritten:**
Only Spec Kit infrastructure files:
- Agent command files (`.claude/commands/`, `.github/prompts/`, etc.)
- Scripts in `.specify/scripts/`
- Templates in `.specify/templates/`
- Memory files in `.specify/memory/` (including constitution)
**What stays untouched:**
- Your `specs/` directory (specifications, plans, tasks)
- Your source code files
- Your `.git/` directory and git history
- Any other files not part of Spec Kit templates
**How to respond:**
- **Type `y` and press Enter** - Proceed with the merge (recommended if upgrading)
- **Type `n` and press Enter** - Cancel the operation
- **Use `--force` flag** - Skip this confirmation entirely:
```bash
specify init --here --force --ai copilot
```
**When you see this warning:**
- ✅ **Expected** when upgrading an existing Spec Kit project
- ✅ **Expected** when adding Spec Kit to an existing codebase
- ⚠️ **Unexpected** if you thought you were creating a new project in an empty directory
**Prevention tip:** Before upgrading, commit or back up your `.specify/memory/constitution.md` if you customized it.
### "CLI upgrade doesn't seem to work"
Verify the installation:
```bash
# Check installed tools
uv tool list
# Should show specify-cli
# Verify path
which specify
# Should point to the uv tool installation directory
```
If not found, reinstall:
```bash
uv tool uninstall specify-cli
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
```
### "Do I need to run specify every time I open my project?"
**Short answer:** No, you only run `specify init` once per project (or when upgrading).
**Explanation:**
The `specify` CLI tool is used for:
- **Initial setup:** `specify init` to bootstrap Spec Kit in your project
- **Upgrades:** `specify init --here --force` to update templates and commands
- **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/`, etc.). Your AI assistant reads these command files directly—no need to run `specify` again.
**If your agent isn't recognizing slash commands:**
1. **Verify command files exist:**
```bash
# For GitHub Copilot
ls -la .github/prompts/
# For Claude
ls -la .claude/commands/
```
2. **Restart your IDE/editor completely** (not just reload window)
3. **Check you're in the correct directory** where you ran `specify init`
4. **For some agents**, you may need to reload the workspace or clear cache
**Related issue:** If Copilot can't open local files or uses PowerShell commands unexpectedly, this is typically an IDE context issue, not related to `specify`. Try:
- Restarting VS Code
- Checking file permissions
- Ensuring the workspace folder is properly opened
---
## Version Compatibility
Spec Kit follows semantic versioning for major releases. The CLI and project files are designed to be compatible within the same major version.
**Best practice:** Keep both CLI and project files in sync by upgrading both together during major version changes.
---
## Next Steps
After upgrading:
- **Test new slash commands:** Run `/speckit.constitution` or another command to verify everything works
- **Review release notes:** Check [GitHub Releases](https://github.com/github/spec-kit/releases) for new features and breaking changes
- **Update workflows:** If new commands were added, update your team's development workflows
- **Check documentation:** Visit [github.io/spec-kit](https://github.github.io/spec-kit/) for updated guides

View File

@@ -1,714 +0,0 @@
# Extension API Reference
Technical reference for Spec Kit extension system APIs and manifest schema.
## Table of Contents
1. [Extension Manifest](#extension-manifest)
2. [Python API](#python-api)
3. [Command File Format](#command-file-format)
4. [Configuration Schema](#configuration-schema)
5. [Hook System](#hook-system)
6. [CLI Commands](#cli-commands)
---
## Extension Manifest
### Schema Version 1.0
File: `extension.yml`
```yaml
schema_version: "1.0" # Required
extension:
id: string # Required, pattern: ^[a-z0-9-]+$
name: string # Required, human-readable name
version: string # Required, semantic version (X.Y.Z)
description: string # Required, brief description (<200 chars)
author: string # Required
repository: string # Required, valid URL
license: string # Required (e.g., "MIT", "Apache-2.0")
homepage: string # Optional, valid URL
requires:
speckit_version: string # Required, version specifier (>=X.Y.Z)
tools: # Optional, array of tool requirements
- name: string # Tool name
version: string # Optional, version specifier
required: boolean # Optional, default: false
provides:
commands: # Required, at least one command
- name: string # Required, pattern: ^speckit\.[a-z0-9-]+\.[a-z0-9-]+$
file: string # Required, relative path to command file
description: string # Required
aliases: [string] # Optional, array of alternate names
config: # Optional, array of config files
- name: string # Config file name
template: string # Template file path
description: string
required: boolean # Default: false
hooks: # Optional, event hooks
event_name: # e.g., "after_tasks", "after_implement"
command: string # Command to execute
optional: boolean # Default: true
prompt: string # Prompt text for optional hooks
description: string # Hook description
condition: string # Optional, condition expression
tags: # Optional, array of tags (2-10 recommended)
- string
defaults: # Optional, default configuration values
key: value # Any YAML structure
```
### Field Specifications
#### `extension.id`
- **Type**: string
- **Pattern**: `^[a-z0-9-]+$`
- **Description**: Unique extension identifier
- **Examples**: `jira`, `linear`, `azure-devops`
- **Invalid**: `Jira`, `my_extension`, `extension.id`
#### `extension.version`
- **Type**: string
- **Format**: Semantic versioning (X.Y.Z)
- **Description**: Extension version
- **Examples**: `1.0.0`, `0.9.5`, `2.1.3`
- **Invalid**: `v1.0`, `1.0`, `1.0.0-beta`
#### `requires.speckit_version`
- **Type**: string
- **Format**: Version specifier
- **Description**: Required spec-kit version range
- **Examples**:
- `>=0.1.0` - Any version 0.1.0 or higher
- `>=0.1.0,<2.0.0` - Version 0.1.x or 1.x
- `==0.1.0` - Exactly 0.1.0
- **Invalid**: `0.1.0`, `>= 0.1.0` (space), `latest`
#### `provides.commands[].name`
- **Type**: string
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
- **Description**: Namespaced command name
- **Format**: `speckit.{extension-id}.{command-name}`
- **Examples**: `speckit.jira.specstoissues`, `speckit.linear.sync`
- **Invalid**: `jira.specstoissues`, `speckit.command`, `speckit.jira.CreateIssues`
#### `hooks`
- **Type**: object
- **Keys**: Event names (e.g., `after_tasks`, `after_implement`, `before_commit`)
- **Description**: Hooks that execute at lifecycle events
- **Events**: Defined by core spec-kit commands
---
## Python API
### ExtensionManifest
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManifest
manifest = ExtensionManifest(Path("extension.yml"))
```
**Properties**:
```python
manifest.id # str: Extension ID
manifest.name # str: Extension name
manifest.version # str: Version
manifest.description # str: Description
manifest.requires_speckit_version # str: Required spec-kit version
manifest.commands # List[Dict]: Command definitions
manifest.hooks # Dict: Hook definitions
```
**Methods**:
```python
manifest.get_hash() # str: SHA256 hash of manifest file
```
**Exceptions**:
```python
ValidationError # Invalid manifest structure
CompatibilityError # Incompatible with current spec-kit version
```
### ExtensionRegistry
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionRegistry
registry = ExtensionRegistry(extensions_dir)
```
**Methods**:
```python
# Add extension to registry
registry.add(extension_id: str, metadata: dict)
# Remove extension from registry
registry.remove(extension_id: str)
# Get extension metadata
metadata = registry.get(extension_id: str) # Optional[dict]
# List all extensions
extensions = registry.list() # Dict[str, dict]
# Check if installed
is_installed = registry.is_installed(extension_id: str) # bool
```
**Registry Format**:
```json
{
"schema_version": "1.0",
"extensions": {
"jira": {
"version": "1.0.0",
"source": "catalog",
"manifest_hash": "sha256...",
"enabled": true,
"registered_commands": ["speckit.jira.specstoissues", ...],
"installed_at": "2026-01-28T..."
}
}
}
```
### ExtensionManager
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionManager
manager = ExtensionManager(project_root)
```
**Methods**:
```python
# Install from directory
manifest = manager.install_from_directory(
source_dir: Path,
speckit_version: str,
register_commands: bool = True
) # Returns: ExtensionManifest
# Install from ZIP
manifest = manager.install_from_zip(
zip_path: Path,
speckit_version: str
) # Returns: ExtensionManifest
# Remove extension
success = manager.remove(
extension_id: str,
keep_config: bool = False
) # Returns: bool
# List installed extensions
extensions = manager.list_installed() # List[Dict]
# Get extension manifest
manifest = manager.get_extension(extension_id: str) # Optional[ExtensionManifest]
# Check compatibility
manager.check_compatibility(
manifest: ExtensionManifest,
speckit_version: str
) # Raises: CompatibilityError if incompatible
```
### ExtensionCatalog
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import ExtensionCatalog
catalog = ExtensionCatalog(project_root)
```
**Methods**:
```python
# Fetch catalog
catalog_data = catalog.fetch_catalog(force_refresh: bool = False) # Dict
# Search extensions
results = catalog.search(
query: Optional[str] = None,
tag: Optional[str] = None,
author: Optional[str] = None,
verified_only: bool = False
) # Returns: List[Dict]
# Get extension info
ext_info = catalog.get_extension_info(extension_id: str) # Optional[Dict]
# Check cache validity
is_valid = catalog.is_cache_valid() # bool
# Clear cache
catalog.clear_cache()
```
### HookExecutor
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import HookExecutor
hook_executor = HookExecutor(project_root)
```
**Methods**:
```python
# Get project config
config = hook_executor.get_project_config() # Dict
# Save project config
hook_executor.save_project_config(config: Dict)
# Register hooks
hook_executor.register_hooks(manifest: ExtensionManifest)
# Unregister hooks
hook_executor.unregister_hooks(extension_id: str)
# Get hooks for event
hooks = hook_executor.get_hooks_for_event(event_name: str) # List[Dict]
# Check if hook should execute
should_run = hook_executor.should_execute_hook(hook: Dict) # bool
# Format hook message
message = hook_executor.format_hook_message(
event_name: str,
hooks: List[Dict]
) # str
```
### CommandRegistrar
**Module**: `specify_cli.extensions`
```python
from specify_cli.extensions import CommandRegistrar
registrar = CommandRegistrar()
```
**Methods**:
```python
# Register commands for Claude Code
registered = registrar.register_commands_for_claude(
manifest: ExtensionManifest,
extension_dir: Path,
project_root: Path
) # Returns: List[str] (command names)
# Parse frontmatter
frontmatter, body = registrar.parse_frontmatter(content: str)
# Render frontmatter
yaml_text = registrar.render_frontmatter(frontmatter: Dict) # str
```
---
## Command File Format
### Universal Command Format
**File**: `commands/{command-name}.md`
```markdown
---
description: "Command description"
tools:
- 'mcp-server/tool_name'
- 'other-mcp-server/other_tool'
---
# Command Title
Command documentation in Markdown.
## Prerequisites
1. Requirement 1
2. Requirement 2
## User Input
$ARGUMENTS
## Steps
### Step 1: Description
Instruction text...
\`\`\`bash
# Shell commands
\`\`\`
### Step 2: Another Step
More instructions...
## Configuration Reference
Information about configuration options.
## Notes
Additional notes and tips.
```
### Frontmatter Fields
```yaml
description: string # Required, brief command description
tools: [string] # Optional, MCP tools required
```
### Special Variables
- `$ARGUMENTS` - Placeholder for user-provided arguments
- Extension context automatically injected:
```markdown
<!-- Extension: {extension-id} -->
<!-- Config: .specify/extensions/{extension-id}/ -->
```
---
## Configuration Schema
### Extension Config File
**File**: `.specify/extensions/{extension-id}/{extension-id}-config.yml`
Extensions define their own config schema. Common patterns:
```yaml
# Connection settings
connection:
url: string
api_key: string
# Project settings
project:
key: string
workspace: string
# Feature flags
features:
enabled: boolean
auto_sync: boolean
# Defaults
defaults:
labels: [string]
assignee: string
# Custom fields
field_mappings:
internal_name: "external_field_id"
```
### Config Layers
1. **Extension Defaults** (from `extension.yml` `defaults` section)
2. **Project Config** (`{extension-id}-config.yml`)
3. **Local Override** (`{extension-id}-config.local.yml`, gitignored)
4. **Environment Variables** (`SPECKIT_{EXTENSION}_*`)
### Environment Variable Pattern
Format: `SPECKIT_{EXTENSION}_{KEY}`
Examples:
- `SPECKIT_JIRA_PROJECT_KEY`
- `SPECKIT_LINEAR_API_KEY`
- `SPECKIT_GITHUB_TOKEN`
---
## Hook System
### Hook Definition
**In extension.yml**:
```yaml
hooks:
after_tasks:
command: "speckit.jira.specstoissues"
optional: true
prompt: "Create Jira issues from tasks?"
description: "Automatically create Jira hierarchy"
condition: null
```
### Hook Events
Standard events (defined by core):
- `after_tasks` - After task generation
- `after_implement` - After implementation
- `before_commit` - Before git commit
- `after_commit` - After git commit
### Hook Configuration
**In `.specify/extensions.yml`**:
```yaml
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
description: "..."
condition: null
```
### Hook Message Format
```markdown
## Extension Hooks
**Optional Hook**: {extension}
Command: `/{command}`
Description: {description}
Prompt: {prompt}
To execute: `/{command}`
```
Or for mandatory hooks:
```markdown
**Automatic Hook**: {extension}
Executing: `/{command}`
EXECUTE_COMMAND: {command}
```
---
## CLI Commands
### extension list
**Usage**: `specify extension list [OPTIONS]`
**Options**:
- `--available` - Show available extensions from catalog
- `--all` - Show both installed and available
**Output**: List of installed extensions with metadata
### extension add
**Usage**: `specify extension add EXTENSION [OPTIONS]`
**Options**:
- `--from URL` - Install from custom URL
- `--dev PATH` - Install from local directory
- `--version VERSION` - Install specific version
- `--no-register` - Skip command registration
**Arguments**:
- `EXTENSION` - Extension name or URL
### extension remove
**Usage**: `specify extension remove EXTENSION [OPTIONS]`
**Options**:
- `--keep-config` - Preserve config files
- `--force` - Skip confirmation
**Arguments**:
- `EXTENSION` - Extension ID
### extension search
**Usage**: `specify extension search [QUERY] [OPTIONS]`
**Options**:
- `--tag TAG` - Filter by tag
- `--author AUTHOR` - Filter by author
- `--verified` - Show only verified extensions
**Arguments**:
- `QUERY` - Optional search query
### extension info
**Usage**: `specify extension info EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
### extension update
**Usage**: `specify extension update [EXTENSION]`
**Arguments**:
- `EXTENSION` - Optional, extension ID (default: all)
### extension enable
**Usage**: `specify extension enable EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
### extension disable
**Usage**: `specify extension disable EXTENSION`
**Arguments**:
- `EXTENSION` - Extension ID
---
## Exceptions
### ValidationError
Raised when extension manifest validation fails.
```python
from specify_cli.extensions import ValidationError
try:
manifest = ExtensionManifest(path)
except ValidationError as e:
print(f"Invalid manifest: {e}")
```
### CompatibilityError
Raised when extension is incompatible with current spec-kit version.
```python
from specify_cli.extensions import CompatibilityError
try:
manager.check_compatibility(manifest, "0.1.0")
except CompatibilityError as e:
print(f"Incompatible: {e}")
```
### ExtensionError
Base exception for all extension-related errors.
```python
from specify_cli.extensions import ExtensionError
try:
manager.install_from_directory(path, "0.1.0")
except ExtensionError as e:
print(f"Extension error: {e}")
```
---
## Version Functions
### version_satisfies
Check if a version satisfies a specifier.
```python
from specify_cli.extensions import version_satisfies
# True if 1.2.3 satisfies >=1.0.0,<2.0.0
satisfied = version_satisfies("1.2.3", ">=1.0.0,<2.0.0") # bool
```
---
## File System Layout
```text
.specify/
├── extensions/
│ ├── .registry # Extension registry (JSON)
│ ├── .cache/ # Catalog cache
│ │ ├── catalog.json
│ │ └── catalog-metadata.json
│ ├── .backup/ # Config backups
│ │ └── {ext}-{config}.yml
│ ├── {extension-id}/ # Extension directory
│ │ ├── extension.yml # Manifest
│ │ ├── {ext}-config.yml # User config
│ │ ├── {ext}-config.local.yml # Local overrides (gitignored)
│ │ ├── {ext}-config.template.yml # Template
│ │ ├── commands/ # Command files
│ │ │ └── *.md
│ │ ├── scripts/ # Helper scripts
│ │ │ └── *.sh
│ │ ├── docs/ # Documentation
│ │ └── README.md
│ └── extensions.yml # Project extension config
└── scripts/ # (existing spec-kit)
.claude/
└── commands/
└── speckit.{ext}.{cmd}.md # Registered commands
```
---
*Last Updated: 2026-01-28*
*API Version: 1.0*
*Spec Kit Version: 0.1.0*

View File

@@ -1,649 +0,0 @@
# Extension Development Guide
A guide for creating Spec Kit extensions.
---
## Quick Start
### 1. Create Extension Directory
```bash
mkdir my-extension
cd my-extension
```
### 2. Create `extension.yml` Manifest
```yaml
schema_version: "1.0"
extension:
id: "my-ext" # Lowercase, alphanumeric + hyphens only
name: "My Extension"
version: "1.0.0" # Semantic versioning
description: "My custom extension"
author: "Your Name"
repository: "https://github.com/you/spec-kit-my-ext"
license: "MIT"
requires:
speckit_version: ">=0.1.0" # Minimum spec-kit version
tools: # Optional: External tools required
- name: "my-tool"
required: true
version: ">=1.0.0"
commands: # Optional: Core commands needed
- "speckit.tasks"
provides:
commands:
- name: "speckit.my-ext.hello" # Must follow pattern: speckit.{ext-id}.{cmd}
file: "commands/hello.md"
description: "Say hello"
aliases: ["speckit.hello"] # Optional aliases
config: # Optional: Config files
- name: "my-ext-config.yml"
template: "my-ext-config.template.yml"
description: "Extension configuration"
required: false
hooks: # Optional: Integration hooks
after_tasks:
command: "speckit.my-ext.hello"
optional: true
prompt: "Run hello command?"
tags: # Optional: For catalog search
- "example"
- "utility"
```
### 3. Create Commands Directory
```bash
mkdir commands
```
### 4. Create Command File
**File**: `commands/hello.md`
```markdown
---
description: "Say hello command"
tools: # Optional: AI tools this command uses
- 'some-tool/function'
scripts: # Optional: Helper scripts
sh: ../../scripts/bash/helper.sh
ps: ../../scripts/powershell/helper.ps1
---
# Hello Command
This command says hello!
## User Input
$ARGUMENTS
## Steps
1. Greet the user
2. Show extension is working
```bash
echo "Hello from my extension!"
echo "Arguments: $ARGUMENTS"
```
## Extension Configuration
Load extension config from `.specify/extensions/my-ext/my-ext-config.yml`.
### 5. Test Locally
```bash
cd /path/to/spec-kit-project
specify extension add --dev /path/to/my-extension
```
### 6. Verify Installation
```bash
specify extension list
# Should show:
# ✓ My Extension (v1.0.0)
# My custom extension
# Commands: 1 | Hooks: 1 | Status: Enabled
```
### 7. Test Command
If using Claude:
```bash
claude
> /speckit.my-ext.hello world
```
The command will be available in `.claude/commands/speckit.my-ext.hello.md`.
---
## Manifest Schema Reference
### Required Fields
#### `schema_version`
Extension manifest schema version. Currently: `"1.0"`
#### `extension`
Extension metadata block.
**Required sub-fields**:
- `id`: Extension identifier (lowercase, alphanumeric, hyphens)
- `name`: Human-readable name
- `version`: Semantic version (e.g., "1.0.0")
- `description`: Short description
**Optional sub-fields**:
- `author`: Extension author
- `repository`: Source code URL
- `license`: SPDX license identifier
- `homepage`: Extension homepage URL
#### `requires`
Compatibility requirements.
**Required sub-fields**:
- `speckit_version`: Semantic version specifier (e.g., ">=0.1.0,<2.0.0")
**Optional sub-fields**:
- `tools`: External tools required (array of tool objects)
- `commands`: Core spec-kit commands needed (array of command names)
- `scripts`: Core scripts required (array of script names)
#### `provides`
What the extension provides.
**Required sub-fields**:
- `commands`: Array of command objects (must have at least one)
**Command object**:
- `name`: Command name (must match `speckit.{ext-id}.{command}`)
- `file`: Path to command file (relative to extension root)
- `description`: Command description (optional)
- `aliases`: Alternative command names (optional, array)
### Optional Fields
#### `hooks`
Integration hooks for automatic execution.
Available hook points:
- `after_tasks`: After `/speckit.tasks` completes
- `after_implement`: After `/speckit.implement` completes (future)
Hook object:
- `command`: Command to execute (must be in `provides.commands`)
- `optional`: If true, prompt user before executing
- `prompt`: Prompt text for optional hooks
- `description`: Hook description
- `condition`: Execution condition (future)
#### `tags`
Array of tags for catalog discovery.
#### `defaults`
Default extension configuration values.
#### `config_schema`
JSON Schema for validating extension configuration.
---
## Command File Format
### Frontmatter (YAML)
```yaml
---
description: "Command description" # Required
tools: # Optional
- 'tool-name/function'
scripts: # Optional
sh: ../../scripts/bash/helper.sh
ps: ../../scripts/powershell/helper.ps1
---
```
### Body (Markdown)
Use standard Markdown with special placeholders:
- `$ARGUMENTS`: User-provided arguments
- `{SCRIPT}`: Replaced with script path during registration
**Example**:
````markdown
## Steps
1. Parse arguments
2. Execute logic
```bash
args="$ARGUMENTS"
echo "Running with args: $args"
```
````
### Script Path Rewriting
Extension commands use relative paths that get rewritten during registration:
**In extension**:
```yaml
scripts:
sh: ../../scripts/bash/helper.sh
```
**After registration**:
```yaml
scripts:
sh: .specify/scripts/bash/helper.sh
```
This allows scripts to reference core spec-kit scripts.
---
## Configuration Files
### Config Template
**File**: `my-ext-config.template.yml`
```yaml
# My Extension Configuration
# Copy this to my-ext-config.yml and customize
# Example configuration
api:
endpoint: "https://api.example.com"
timeout: 30
features:
feature_a: true
feature_b: false
credentials:
# DO NOT commit credentials!
# Use environment variables instead
api_key: "${MY_EXT_API_KEY}"
```
### Config Loading
In your command, load config with layered precedence:
1. Extension defaults (`extension.yml` → `defaults`)
2. Project config (`.specify/extensions/my-ext/my-ext-config.yml`)
3. Local overrides (`.specify/extensions/my-ext/my-ext-config.local.yml` - gitignored)
4. Environment variables (`SPECKIT_MY_EXT_*`)
**Example loading script**:
```bash
#!/usr/bin/env bash
EXT_DIR=".specify/extensions/my-ext"
# Load and merge config
config=$(yq eval '.' "$EXT_DIR/my-ext-config.yml" -o=json)
# Apply env overrides
if [ -n "${SPECKIT_MY_EXT_API_KEY:-}" ]; then
config=$(echo "$config" | jq ".api.api_key = \"$SPECKIT_MY_EXT_API_KEY\"")
fi
echo "$config"
```
---
## Validation Rules
### Extension ID
- **Pattern**: `^[a-z0-9-]+$`
- **Valid**: `my-ext`, `tool-123`, `awesome-plugin`
- **Invalid**: `MyExt` (uppercase), `my_ext` (underscore), `my ext` (space)
### Extension Version
- **Format**: Semantic versioning (MAJOR.MINOR.PATCH)
- **Valid**: `1.0.0`, `0.1.0`, `2.5.3`
- **Invalid**: `1.0`, `v1.0.0`, `1.0.0-beta`
### Command Name
- **Pattern**: `^speckit\.[a-z0-9-]+\.[a-z0-9-]+$`
- **Valid**: `speckit.my-ext.hello`, `speckit.tool.cmd`
- **Invalid**: `my-ext.hello` (missing prefix), `speckit.hello` (no extension namespace)
### Command File Path
- **Must be** relative to extension root
- **Valid**: `commands/hello.md`, `commands/subdir/cmd.md`
- **Invalid**: `/absolute/path.md`, `../outside.md`
---
## Testing Extensions
### Manual Testing
1. **Create test extension**
2. **Install locally**:
```bash
specify extension add --dev /path/to/extension
```
3. **Verify installation**:
```bash
specify extension list
```
4. **Test commands** with your AI agent
5. **Check command registration**:
```bash
ls .claude/commands/speckit.my-ext.*
```
6. **Remove extension**:
```bash
specify extension remove my-ext
```
### Automated Testing
Create tests for your extension:
```python
# tests/test_my_extension.py
import pytest
from pathlib import Path
from specify_cli.extensions import ExtensionManifest
def test_manifest_valid():
"""Test extension manifest is valid."""
manifest = ExtensionManifest(Path("extension.yml"))
assert manifest.id == "my-ext"
assert len(manifest.commands) >= 1
def test_command_files_exist():
"""Test all command files exist."""
manifest = ExtensionManifest(Path("extension.yml"))
for cmd in manifest.commands:
cmd_file = Path(cmd["file"])
assert cmd_file.exists(), f"Command file not found: {cmd_file}"
```
---
## Distribution
### Option 1: GitHub Repository
1. **Create repository**: `spec-kit-my-ext`
2. **Add files**:
```text
spec-kit-my-ext/
├── extension.yml
├── commands/
├── scripts/
├── docs/
├── README.md
├── LICENSE
└── CHANGELOG.md
```
3. **Create release**: Tag with version (e.g., `v1.0.0`)
4. **Install from repo**:
```bash
git clone https://github.com/you/spec-kit-my-ext
specify extension add --dev spec-kit-my-ext/
```
### Option 2: ZIP Archive (Future)
Create ZIP archive and host on GitHub Releases:
```bash
zip -r spec-kit-my-ext-1.0.0.zip extension.yml commands/ scripts/ docs/
```
Users install with:
```bash
specify extension add --from https://github.com/.../spec-kit-my-ext-1.0.0.zip
```
### Option 3: Extension Catalog (Future)
Submit to official catalog:
1. **Fork** spec-kit repository
2. **Add entry** to `extensions/catalog.json`
3. **Create PR**
4. **After merge**, users can install with:
```bash
specify extension add my-ext # No URL needed!
```
---
## Best Practices
### Naming Conventions
- **Extension ID**: Use descriptive, hyphenated names (`jira-integration`, not `ji`)
- **Commands**: Use verb-noun pattern (`create-issue`, `sync-status`)
- **Config files**: Match extension ID (`jira-config.yml`)
### Documentation
- **README.md**: Overview, installation, usage
- **CHANGELOG.md**: Version history
- **docs/**: Detailed guides
- **Command descriptions**: Clear, concise
### Versioning
- **Follow SemVer**: `MAJOR.MINOR.PATCH`
- **MAJOR**: Breaking changes
- **MINOR**: New features
- **PATCH**: Bug fixes
### Security
- **Never commit secrets**: Use environment variables
- **Validate input**: Sanitize user arguments
- **Document permissions**: What files/APIs are accessed
### Compatibility
- **Specify version range**: Don't require exact version
- **Test with multiple versions**: Ensure compatibility
- **Graceful degradation**: Handle missing features
---
## Example Extensions
### Minimal Extension
Smallest possible extension:
```yaml
# extension.yml
schema_version: "1.0"
extension:
id: "minimal"
name: "Minimal Extension"
version: "1.0.0"
description: "Minimal example"
requires:
speckit_version: ">=0.1.0"
provides:
commands:
- name: "speckit.minimal.hello"
file: "commands/hello.md"
```
````markdown
<!-- commands/hello.md -->
---
description: "Hello command"
---
# Hello World
```bash
echo "Hello, $ARGUMENTS!"
```
````
### Extension with Config
Extension using configuration:
```yaml
# extension.yml
# ... metadata ...
provides:
config:
- name: "tool-config.yml"
template: "tool-config.template.yml"
required: true
```
```yaml
# tool-config.template.yml
api_endpoint: "https://api.example.com"
timeout: 30
```
````markdown
<!-- commands/use-config.md -->
# Use Config
Load config:
```bash
config_file=".specify/extensions/tool/tool-config.yml"
endpoint=$(yq eval '.api_endpoint' "$config_file")
echo "Using endpoint: $endpoint"
```
````
### Extension with Hooks
Extension that runs automatically:
```yaml
# extension.yml
hooks:
after_tasks:
command: "speckit.auto.analyze"
optional: false # Always run
description: "Analyze tasks after generation"
```
---
## Troubleshooting
### Extension won't install
**Error**: `Invalid extension ID`
- **Fix**: Use lowercase, alphanumeric + hyphens only
**Error**: `Extension requires spec-kit >=0.2.0`
- **Fix**: Update spec-kit with `uv tool install specify-cli --force`
**Error**: `Command file not found`
- **Fix**: Ensure command files exist at paths specified in manifest
### Commands not registered
**Symptom**: Commands don't appear in AI agent
**Check**:
1. `.claude/commands/` directory exists
2. Extension installed successfully
3. Commands registered in registry:
```bash
cat .specify/extensions/.registry
```
**Fix**: Reinstall extension to trigger registration
### Config not loading
**Check**:
1. Config file exists: `.specify/extensions/{ext-id}/{ext-id}-config.yml`
2. YAML syntax is valid: `yq eval '.' config.yml`
3. Environment variables set correctly
---
## Getting Help
- **Issues**: Report bugs at GitHub repository
- **Discussions**: Ask questions in GitHub Discussions
- **Examples**: See `spec-kit-jira` for full-featured example (Phase B)
---
## Next Steps
1. **Create your extension** following this guide
2. **Test locally** with `--dev` flag
3. **Share with community** (GitHub, catalog)
4. **Iterate** based on feedback
Happy extending! 🚀

View File

@@ -1,530 +0,0 @@
# Extension Publishing Guide
This guide explains how to publish your extension to the Spec Kit extension catalog, making it discoverable by `specify extension search`.
## Table of Contents
1. [Prerequisites](#prerequisites)
2. [Prepare Your Extension](#prepare-your-extension)
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 an extension, ensure you have:
1. **Valid Extension**: A working extension with a valid `extension.yml` manifest
2. **Git Repository**: Extension hosted on GitHub (or other public git hosting)
3. **Documentation**: README.md with installation 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**: Extension tested on real projects
---
## Prepare Your Extension
### 1. Extension Structure
Ensure your extension follows the standard structure:
```text
your-extension/
├── extension.yml # Required: Extension manifest
├── README.md # Required: Documentation
├── LICENSE # Required: License file
├── CHANGELOG.md # Recommended: Version history
├── .gitignore # Recommended: Git ignore rules
├── commands/ # Extension commands
│ ├── command1.md
│ └── command2.md
├── config-template.yml # Config template (if needed)
└── docs/ # Additional documentation
├── usage.md
└── examples/
```
### 2. extension.yml Validation
Verify your manifest is valid:
```yaml
schema_version: "1.0"
extension:
id: "your-extension" # Unique lowercase-hyphenated ID
name: "Your Extension 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-your-extension"
license: "MIT"
homepage: "https://github.com/your-org/spec-kit-your-extension"
requires:
speckit_version: ">=0.1.0" # Required spec-kit version
provides:
commands: # List all commands
- name: "speckit.your-extension.command"
file: "commands/command.md"
description: "Command description"
tags: # 2-5 relevant tags
- "category"
- "tool-name"
```
**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 100 characters)
-`repository` URL is valid and public
- ✅ All command files exist in the extension directory
- ✅ Tags are lowercase and descriptive
### 3. Create GitHub Release
Create a GitHub release for your extension version:
```bash
# Tag the release
git tag v1.0.0
git push origin v1.0.0
# Create release on GitHub
# Go to: https://github.com/your-org/spec-kit-your-extension/releases/new
# - Tag: v1.0.0
# - Title: v1.0.0 - Release Name
# - Description: Changelog/release notes
```
The release archive URL will be:
```text
https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
```
### 4. Test Installation
Test that users can install from your release:
```bash
# Test dev installation
specify extension add --dev /path/to/your-extension
# Test from GitHub archive
specify extension add --from https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip
```
---
## Submit to Catalog
### 1. Fork the spec-kit Repository
```bash
# Fork on GitHub
# https://github.com/statsperform/spec-kit/fork
# Clone your fork
git clone https://github.com/YOUR-USERNAME/spec-kit.git
cd spec-kit
```
### 2. Add Extension to Catalog
Edit `extensions/catalog.json` and add your extension:
```json
{
"schema_version": "1.0",
"updated_at": "2026-01-28T15:54:00Z",
"catalog_url": "https://raw.githubusercontent.com/statsperform/spec-kit/main/extensions/catalog.json",
"extensions": {
"your-extension": {
"name": "Your Extension Name",
"id": "your-extension",
"description": "Brief description of your extension",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-your-extension",
"homepage": "https://github.com/your-org/spec-kit-your-extension",
"documentation": "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/",
"changelog": "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "required-mcp-tool",
"version": ">=1.0.0",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"category",
"tool-name",
"feature"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-01-28T00:00:00Z",
"updated_at": "2026-01-28T00:00:00Z"
}
}
}
```
**Important**:
- Set `verified: false` (maintainers will verify)
- Set `downloads: 0` and `stars: 0` (auto-updated later)
- Use current timestamp for `created_at` and `updated_at`
- Update the top-level `updated_at` to current time
### 3. Submit Pull Request
```bash
# Create a branch
git checkout -b add-your-extension
# Commit your changes
git add extensions/catalog.json
git commit -m "Add your-extension to catalog
- Extension ID: your-extension
- Version: 1.0.0
- Author: Your Name
- Description: Brief description
"
# Push to your fork
git push origin add-your-extension
# Create Pull Request on GitHub
# https://github.com/statsperform/spec-kit/compare
```
**Pull Request Template**:
```markdown
## Extension Submission
**Extension Name**: Your Extension Name
**Extension ID**: your-extension
**Version**: 1.0.0
**Author**: Your Name
**Repository**: https://github.com/your-org/spec-kit-your-extension
### Description
Brief description of what your extension does.
### Checklist
- [x] Valid extension.yml manifest
- [x] README.md with installation and usage docs
- [x] LICENSE file included
- [x] GitHub release created (v1.0.0)
- [x] Extension tested on real project
- [x] All commands working
- [x] No security vulnerabilities
### Testing
Tested on:
- macOS 13.0+ with spec-kit 0.1.0
- Project: [Your test project]
### Additional Notes
Any additional context or notes for reviewers.
```
---
## Verification Process
### What Happens After Submission
1. **Automated Checks** (if available):
- Manifest validation
- Download URL accessibility
- Repository existence
- License file presence
2. **Manual Review**:
- Code quality review
- Security audit
- Functionality testing
- Documentation review
3. **Verification**:
- If approved, `verified: true` is set
- Extension appears in `specify extension search --verified`
### Verification Criteria
To be verified, your extension must:
**Functionality**:
- Works as described in documentation
- All commands execute without errors
- No breaking changes to user workflows
**Security**:
- No known vulnerabilities
- No malicious code
- Safe handling of user data
- Proper validation of inputs
**Code Quality**:
- Clean, readable code
- Follows extension best practices
- Proper error handling
- Helpful error messages
**Documentation**:
- Clear installation instructions
- Usage examples
- Troubleshooting section
- Accurate description
**Maintenance**:
- Active repository
- Responsive to issues
- Regular updates
- Semantic versioning followed
### Typical Review Timeline
- **Automated checks**: Immediate (if implemented)
- **Manual review**: 3-7 business days
- **Verification**: After successful review
---
## Release Workflow
### Publishing New Versions
When releasing a new version:
1. **Update version** in `extension.yml`:
```yaml
extension:
version: "1.1.0" # Updated version
```
2. **Update CHANGELOG.md**:
```markdown
## [1.1.0] - 2026-02-15
### Added
- New feature X
### Fixed
- Bug fix Y
```
3. **Create GitHub release**:
```bash
git tag v1.1.0
git push origin v1.1.0
# Create release on GitHub
```
4. **Update catalog**:
```bash
# Fork spec-kit repo (or update existing fork)
cd spec-kit
# Update extensions/catalog.json
jq '.extensions["your-extension"].version = "1.1.0"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].download_url = "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.1.0.zip"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
# Submit PR
git checkout -b update-your-extension-v1.1.0
git add extensions/catalog.json
git commit -m "Update your-extension to v1.1.0"
git push origin update-your-extension-v1.1.0
```
5. **Submit update PR** with changelog in description
---
## Best Practices
### Extension Design
1. **Single Responsibility**: Each extension should focus on one tool/integration
2. **Clear Naming**: Use descriptive, unambiguous names
3. **Minimal Dependencies**: Avoid unnecessary dependencies
4. **Backward Compatibility**: Follow semantic versioning strictly
### Documentation
1. **README.md Structure**:
- Overview and features
- Installation instructions
- Configuration guide
- Usage examples
- Troubleshooting
- Contributing guidelines
2. **Command Documentation**:
- Clear description
- Prerequisites listed
- Step-by-step instructions
- Error handling guidance
- Examples
3. **Configuration**:
- Provide template file
- Document all options
- Include examples
- Explain defaults
### Security
1. **Input Validation**: Validate all user inputs
2. **No Hardcoded Secrets**: Never include credentials
3. **Safe Dependencies**: Only use trusted dependencies
4. **Audit Regularly**: Check for vulnerabilities
### Maintenance
1. **Respond to Issues**: Address issues within 1-2 weeks
2. **Regular Updates**: Keep dependencies updated
3. **Changelog**: Maintain detailed changelog
4. **Deprecation**: Give advance notice for breaking changes
### Community
1. **License**: Use permissive open-source license (MIT, Apache 2.0)
2. **Contributing**: Welcome contributions
3. **Code of Conduct**: Be respectful and inclusive
4. **Support**: Provide ways to get help (issues, discussions, email)
---
## FAQ
### Q: Can I publish private/proprietary extensions?
A: The main catalog is for public extensions only. For private extensions:
- Host your own catalog.json file
- Users add your catalog: `specify extension add-catalog https://your-domain.com/catalog.json`
- Not yet implemented - coming in Phase 4
### Q: How long does verification take?
A: Typically 3-7 business days for initial review. Updates to verified extensions are usually faster.
### Q: What if my extension is rejected?
A: You'll receive feedback on what needs to be fixed. Make the changes and resubmit.
### Q: Can I update my extension anytime?
A: Yes, submit a PR to update the catalog with your new version. Verified status may be re-evaluated for major changes.
### Q: Do I need to be verified to be in the catalog?
A: No, unverified extensions are still searchable. Verification just adds trust and visibility.
### Q: Can extensions have paid features?
A: Extensions should be free and open-source. Commercial support/services are allowed, but core functionality must be free.
---
## Support
- **Catalog Issues**: <https://github.com/statsperform/spec-kit/issues>
- **Extension Template**: <https://github.com/statsperform/spec-kit-extension-template> (coming soon)
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
- **Community**: Discussions and Q&A
---
## Appendix: Catalog Schema
### Complete Catalog Entry Schema
```json
{
"name": "string (required)",
"id": "string (required, unique)",
"description": "string (required, <200 chars)",
"author": "string (required)",
"version": "string (required, semver)",
"download_url": "string (required, valid URL)",
"repository": "string (required, valid URL)",
"homepage": "string (optional, valid URL)",
"documentation": "string (optional, valid URL)",
"changelog": "string (optional, valid URL)",
"license": "string (required)",
"requires": {
"speckit_version": "string (required, version specifier)",
"tools": [
{
"name": "string (required)",
"version": "string (optional, version specifier)",
"required": "boolean (default: false)"
}
]
},
"provides": {
"commands": "integer (optional)",
"hooks": "integer (optional)"
},
"tags": ["array of strings (2-10 tags)"],
"verified": "boolean (default: false)",
"downloads": "integer (auto-updated)",
"stars": "integer (auto-updated)",
"created_at": "string (ISO 8601 datetime)",
"updated_at": "string (ISO 8601 datetime)"
}
```
### Valid Tags
Recommended tag categories:
- **Integration**: jira, linear, github, gitlab, azure-devops
- **Category**: issue-tracking, vcs, ci-cd, documentation, testing
- **Platform**: atlassian, microsoft, google
- **Feature**: automation, reporting, deployment, monitoring
Use 2-5 tags that best describe your extension.
---
*Last Updated: 2026-01-28*
*Catalog Format Version: 1.0*

View File

@@ -1,885 +0,0 @@
# Extension User Guide
Complete guide for using Spec Kit extensions to enhance your workflow.
## Table of Contents
1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
3. [Finding Extensions](#finding-extensions)
4. [Installing Extensions](#installing-extensions)
5. [Using Extensions](#using-extensions)
6. [Managing Extensions](#managing-extensions)
7. [Configuration](#configuration)
8. [Troubleshooting](#troubleshooting)
9. [Best Practices](#best-practices)
---
## Introduction
### What are Extensions?
Extensions are modular packages that add new commands and functionality to Spec Kit without bloating the core framework. They allow you to:
- **Integrate** with external tools (Jira, Linear, GitHub, etc.)
- **Automate** repetitive tasks with hooks
- **Customize** workflows for your team
- **Share** solutions across projects
### Why Use Extensions?
- **Clean Core**: Keeps spec-kit lightweight and focused
- **Optional Features**: Only install what you need
- **Community Driven**: Anyone can create and share extensions
- **Version Controlled**: Extensions are versioned independently
---
## Getting Started
### Prerequisites
- Spec Kit version 0.1.0 or higher
- A spec-kit project (directory with `.specify/` folder)
### Check Your Version
```bash
specify version
# Should show 0.1.0 or higher
```
### First Extension
Let's install the Jira extension as an example:
```bash
# 1. Search for the extension
specify extension search jira
# 2. Get detailed information
specify extension info jira
# 3. Install it
specify extension add jira
# 4. Configure it
vim .specify/extensions/jira/jira-config.yml
# 5. Use it
# (Commands are now available in Claude Code)
/speckit.jira.specstoissues
```
---
## Finding Extensions
### Browse All Extensions
```bash
specify extension search
```
Shows all available extensions in the catalog.
### Search by Keyword
```bash
# Search for "jira"
specify extension search jira
# Search for "issue tracking"
specify extension search issue
```
### Filter by Tag
```bash
# Find all issue-tracking extensions
specify extension search --tag issue-tracking
# Find all Atlassian tools
specify extension search --tag atlassian
```
### Filter by Author
```bash
# Extensions by Stats Perform
specify extension search --author "Stats Perform"
```
### Show Verified Only
```bash
# Only show verified extensions
specify extension search --verified
```
### Get Extension Details
```bash
# Detailed information
specify extension info jira
```
Shows:
- Description
- Requirements
- Commands provided
- Hooks available
- Links (documentation, repository, changelog)
- Installation status
---
## Installing Extensions
### Install from Catalog
```bash
# By name (from catalog)
specify extension add jira
```
This will:
1. Download the extension from GitHub
2. Validate the manifest
3. Check compatibility with your spec-kit version
4. Install to `.specify/extensions/jira/`
5. Register commands with your AI agent
6. Create config template
### Install from URL
```bash
# From GitHub release
specify extension add --from https://github.com/org/spec-kit-ext/archive/refs/tags/v1.0.0.zip
```
### Install from Local Directory (Development)
```bash
# For testing or development
specify extension add --dev /path/to/extension
```
### Installation Output
```text
✓ Extension installed successfully!
Jira Integration (v1.0.0)
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Provided commands:
• speckit.jira.specstoissues - Create Jira hierarchy from spec and tasks
• speckit.jira.discover-fields - Discover Jira custom fields for configuration
• speckit.jira.sync-status - Sync task completion status to Jira
⚠ Configuration may be required
Check: .specify/extensions/jira/
```
---
## Using Extensions
### Using Extension Commands
Extensions add commands that appear in your AI agent (Claude Code):
```text
# In Claude Code
> /speckit.jira.specstoissues
# Or use short alias (if provided)
> /speckit.specstoissues
```
### Extension Configuration
Most extensions require configuration:
```bash
# 1. Find the config file
ls .specify/extensions/jira/
# 2. Copy template to config
cp .specify/extensions/jira/jira-config.template.yml \
.specify/extensions/jira/jira-config.yml
# 3. Edit configuration
vim .specify/extensions/jira/jira-config.yml
# 4. Use the extension
# (Commands will now work with your config)
```
### Extension Hooks
Some extensions provide hooks that execute after core commands:
**Example**: Jira extension hooks into `/speckit.tasks`
```text
# Run core command
> /speckit.tasks
# Output includes:
## Extension Hooks
**Optional Hook**: jira
Command: `/speckit.jira.specstoissues`
Description: Automatically create Jira hierarchy after task generation
Prompt: Create Jira issues from tasks?
To execute: `/speckit.jira.specstoissues`
```
You can then choose to run the hook or skip it.
---
## Managing Extensions
### List Installed Extensions
```bash
specify extension list
```
Output:
```text
Installed Extensions:
✓ Jira Integration (v1.0.0)
Create Jira Epics, Stories, and Issues from spec-kit artifacts
Commands: 3 | Hooks: 1 | Status: Enabled
```
### Update Extensions
```bash
# Check for updates (all extensions)
specify extension update
# Update specific extension
specify extension update jira
```
Output:
```text
🔄 Checking for updates...
Updates available:
• jira: 1.0.0 → 1.1.0
Update these extensions? [y/N]:
```
### Disable Extension Temporarily
```bash
# Disable without removing
specify extension disable jira
✓ Extension 'jira' disabled
Commands will no longer be available. Hooks will not execute.
To re-enable: specify extension enable jira
```
### Re-enable Extension
```bash
specify extension enable jira
✓ Extension 'jira' enabled
```
### Remove Extension
```bash
# Remove extension (with confirmation)
specify extension remove jira
# Keep configuration when removing
specify extension remove jira --keep-config
# Force removal (no confirmation)
specify extension remove jira --force
```
---
## Configuration
### Configuration Files
Extensions can have multiple configuration files:
```text
.specify/extensions/jira/
├── jira-config.yml # Main config (version controlled)
├── jira-config.local.yml # Local overrides (gitignored)
└── jira-config.template.yml # Template (reference)
```
### Configuration Layers
Configuration is merged in this order (highest priority last):
1. **Extension defaults** (from `extension.yml`)
2. **Project config** (`jira-config.yml`)
3. **Local overrides** (`jira-config.local.yml`)
4. **Environment variables** (`SPECKIT_JIRA_*`)
### Example: Jira Configuration
**Project config** (`.specify/extensions/jira/jira-config.yml`):
```yaml
project:
key: "MSATS"
defaults:
epic:
labels: ["spec-driven"]
```
**Local override** (`.specify/extensions/jira/jira-config.local.yml`):
```yaml
project:
key: "MYTEST" # Override for local development
```
**Environment variable**:
```bash
export SPECKIT_JIRA_PROJECT_KEY="DEVTEST"
```
Final resolved config uses `DEVTEST` from environment variable.
### Project-Wide Extension Settings
File: `.specify/extensions.yml`
```yaml
# Extensions installed in this project
installed:
- jira
- linear
# Global settings
settings:
auto_execute_hooks: true
# Hook configuration
hooks:
after_tasks:
- extension: jira
command: speckit.jira.specstoissues
enabled: true
optional: true
prompt: "Create Jira issues from tasks?"
```
### Core Environment Variables
In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`), spec-kit supports core environment variables:
| Variable | Description | Default |
|----------|-------------|---------|
| `SPECKIT_CATALOG_URL` | Override the extension catalog URL | GitHub-hosted catalog |
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
#### Example: Using a custom catalog for testing
```bash
# Point to a local or alternative catalog
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
# Or use a staging catalog
export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
```
---
## Organization Catalog Customization
### Why the Default Catalog is Empty
The default spec-kit catalog ships empty by design. This allows organizations to:
- **Control available extensions** - Curate which extensions your team can install
- **Host private extensions** - Internal tools that shouldn't be public
- **Customize for compliance** - Meet security/audit requirements
- **Support air-gapped environments** - Work without internet access
### Setting Up a Custom Catalog
#### 1. Create Your Catalog File
Create a `catalog.json` file with your extensions:
```json
{
"schema_version": "1.0",
"updated_at": "2026-02-03T00:00:00Z",
"catalog_url": "https://your-org.com/spec-kit/catalog.json",
"extensions": {
"jira": {
"name": "Jira Integration",
"id": "jira",
"description": "Create Jira issues from spec-kit artifacts",
"author": "Your Organization",
"version": "2.1.0",
"download_url": "https://github.com/your-org/spec-kit-jira/archive/refs/tags/v2.1.0.zip",
"repository": "https://github.com/your-org/spec-kit-jira",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{"name": "atlassian-mcp-server", "required": true}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": ["jira", "atlassian", "issue-tracking"],
"verified": true
},
"internal-tool": {
"name": "Internal Tool Integration",
"id": "internal-tool",
"description": "Connect to internal company systems",
"author": "Your Organization",
"version": "1.0.0",
"download_url": "https://internal.your-org.com/extensions/internal-tool-1.0.0.zip",
"repository": "https://github.internal.your-org.com/spec-kit-internal",
"license": "Proprietary",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2
},
"tags": ["internal", "proprietary"],
"verified": true
}
}
}
```
#### 2. Host the Catalog
Options for hosting your catalog:
| Method | URL Example | Use Case |
| ------ | ----------- | -------- |
| GitHub Pages | `https://your-org.github.io/spec-kit-catalog/catalog.json` | Public or org-visible |
| Internal web server | `https://internal.company.com/spec-kit/catalog.json` | Corporate network |
| S3/Cloud storage | `https://s3.amazonaws.com/your-bucket/catalog.json` | Cloud-hosted teams |
| Local file server | `http://localhost:8000/catalog.json` | Development/testing |
**Security requirement**: URLs must use HTTPS (except `localhost` for testing).
#### 3. Configure Your Environment
##### Option A: Environment variable (recommended for CI/CD)
```bash
# In ~/.bashrc, ~/.zshrc, or CI pipeline
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
```bash
# Search should now show your catalog's extensions
specify extension search
# Install from your catalog
specify extension add jira
```
### Catalog JSON Schema
Required fields for each extension entry:
| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `name` | string | Yes | Human-readable name |
| `id` | string | Yes | Unique identifier (lowercase, hyphens) |
| `version` | string | Yes | Semantic version (X.Y.Z) |
| `download_url` | string | Yes | URL to ZIP archive |
| `repository` | string | Yes | Source code URL |
| `description` | string | No | Brief description |
| `author` | string | No | Author/organization |
| `license` | string | No | SPDX license identifier |
| `requires.speckit_version` | string | No | Version constraint |
| `requires.tools` | array | No | Required external tools |
| `provides.commands` | number | No | Number of commands |
| `provides.hooks` | number | No | Number of hooks |
| `tags` | array | No | Search tags |
| `verified` | boolean | No | Verification status |
### Use Cases
#### Private/Internal Extensions
Host proprietary extensions that integrate with internal systems:
```json
{
"internal-auth": {
"name": "Internal SSO Integration",
"download_url": "https://artifactory.company.com/spec-kit/internal-auth-1.0.0.zip",
"verified": true
}
}
```
#### Curated Team Catalog
Limit which extensions your team can install:
```json
{
"extensions": {
"jira": { "..." },
"github": { "..." }
}
}
```
Only `jira` and `github` will appear in `specify extension search`.
#### Air-Gapped Environments
For networks without internet access:
1. Download extension ZIPs to internal file server
2. Create catalog pointing to internal URLs
3. Host catalog on internal web server
```json
{
"jira": {
"download_url": "https://files.internal/spec-kit/jira-2.1.0.zip"
}
}
```
#### Development/Testing
Test new extensions before publishing:
```bash
# Start local server
python -m http.server 8000 --directory ./my-catalog/
# Point spec-kit to local catalog
export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
# Test installation
specify extension add my-new-extension
```
### Combining with Direct Installation
You can still install extensions not in your catalog using `--from`:
```bash
# From catalog
specify extension add jira
# Direct URL (bypasses catalog)
specify extension add --from https://github.com/someone/spec-kit-ext/archive/v1.0.0.zip
# Local development
specify extension add --dev /path/to/extension
```
**Note**: Direct URL installation shows a security warning since the extension isn't from your configured catalog.
---
## Troubleshooting
### Extension Not Found
**Error**: `Extension 'jira' not found in catalog
**Solutions**:
1. Check spelling: `specify extension search jira`
2. Refresh catalog: `specify extension search --help`
3. Check internet connection
4. Extension may not be published yet
### Configuration Not Found
**Error**: `Jira configuration not found`
**Solutions**:
1. Check if extension is installed: `specify extension list`
2. Create config from template:
```bash
cp .specify/extensions/jira/jira-config.template.yml \
.specify/extensions/jira/jira-config.yml
```
3. Reinstall extension: `specify extension remove jira && specify extension add jira`
### Command Not Available
**Issue**: Extension command not appearing in AI agent
**Solutions**:
1. Check extension is enabled: `specify extension list`
2. Restart AI agent (Claude Code)
3. Check command file exists:
```bash
ls .claude/commands/speckit.jira.*.md
```
4. Reinstall extension
### Incompatible Version
**Error**: `Extension requires spec-kit >=0.2.0, but you have 0.1.0`
**Solutions**:
1. Upgrade spec-kit:
```bash
uv tool upgrade specify-cli
```
2. Install older version of extension:
```bash
specify extension add --from https://github.com/org/ext/archive/v1.0.0.zip
```
### MCP Tool Not Available
**Error**: `Tool 'jira-mcp-server/epic_create' not found`
**Solutions**:
1. Check MCP server is installed
2. Check AI agent MCP configuration
3. Restart AI agent
4. Check extension requirements: `specify extension info jira`
### Permission Denied
**Error**: `Permission denied` when accessing Jira
**Solutions**:
1. Check Jira credentials in MCP server config
2. Verify project permissions in Jira
3. Test MCP server connection independently
---
## Best Practices
### 1. Version Control
**Do commit**:
- `.specify/extensions.yml` (project extension config)
- `.specify/extensions/*/jira-config.yml` (project config)
**Don't commit**:
- `.specify/extensions/.cache/` (catalog cache)
- `.specify/extensions/.backup/` (config backups)
- `.specify/extensions/*/*.local.yml` (local overrides)
- `.specify/extensions/.registry` (installation state)
Add to `.gitignore`:
```gitignore
.specify/extensions/.cache/
.specify/extensions/.backup/
.specify/extensions/*/*.local.yml
.specify/extensions/.registry
```
### 2. Team Workflows
**For teams**:
1. Agree on which extensions to use
2. Commit extension configuration
3. Document extension usage in README
4. Keep extensions updated together
**Example README section**:
```markdown
## Extensions
This project uses:
- **jira** (v1.0.0) - Jira integration
- Config: `.specify/extensions/jira/jira-config.yml`
- Requires: jira-mcp-server
To install: `specify extension add jira`
```
### 3. Local Development
Use local config for development:
```yaml
# .specify/extensions/jira/jira-config.local.yml
project:
key: "DEVTEST" # Your test project
defaults:
task:
custom_fields:
customfield_10002: 1 # Lower story points for testing
```
### 4. Environment-Specific Config
Use environment variables for CI/CD:
```bash
# .github/workflows/deploy.yml
env:
SPECKIT_JIRA_PROJECT_KEY: ${{ secrets.JIRA_PROJECT }}
- name: Create Jira Issues
run: specify extension add jira && ...
```
### 5. Extension Updates
**Check for updates regularly**:
```bash
# Weekly or before major releases
specify extension update
```
**Pin versions for stability**:
```yaml
# .specify/extensions.yml
installed:
- id: jira
version: "1.0.0" # Pin to specific version
```
### 6. Minimal Extensions
Only install extensions you actively use:
- Reduces complexity
- Faster command loading
- Less configuration
### 7. Documentation
Document extension usage in your project:
```markdown
# PROJECT.md
## Working with Jira
After creating tasks, sync to Jira:
1. Run `/speckit.tasks` to generate tasks
2. Run `/speckit.jira.specstoissues` to create Jira issues
3. Run `/speckit.jira.sync-status` to update status
```
---
## FAQ
### Q: Can I use multiple extensions at once?
**A**: Yes! Extensions are designed to work together. Install as many as you need.
### Q: Do extensions slow down spec-kit?
**A**: No. Extensions are loaded on-demand and only when their commands are used.
### Q: Can I create private extensions?
**A**: Yes. Install with `--dev` or `--from` and keep private. Public catalog submission is optional.
### Q: How do I know if an extension is safe?
**A**: Look for the ✓ Verified badge. Verified extensions are reviewed by maintainers. Always review extension code before installing.
### Q: Can extensions modify spec-kit core?
**A**: No. Extensions can only add commands and hooks. They cannot modify core functionality.
### Q: What happens if two extensions have the same command name?
**A**: Extensions use namespaced commands (`speckit.{extension}.{command}`), so conflicts are very rare. The extension system will warn you if conflicts occur.
### Q: Can I contribute to existing extensions?
**A**: Yes! Most extensions are open source. Check the repository link in `specify extension info {extension}`.
### Q: How do I report extension bugs?
**A**: Go to the extension's repository (shown in `specify extension info`) and create an issue.
### Q: Can extensions work offline?
**A**: Once installed, extensions work offline. However, some extensions may require internet for their functionality (e.g., Jira requires Jira API access).
### Q: How do I backup my extension configuration?
**A**: Extension configs are in `.specify/extensions/{extension}/`. Back up this directory or commit configs to git.
---
## Support
- **Extension Issues**: Report to extension repository (see `specify extension info`)
- **Spec Kit Issues**: <https://github.com/statsperform/spec-kit/issues>
- **Extension Catalog**: <https://github.com/statsperform/spec-kit/tree/main/extensions>
- **Documentation**: See EXTENSION-DEVELOPMENT-GUIDE.md and EXTENSION-PUBLISHING-GUIDE.md
---
*Last Updated: 2026-01-28*
*Spec Kit Version: 0.1.0*

View File

@@ -1,14 +0,0 @@
# Spec Kit Community Extensions
Community-contributed extensions for [Spec Kit](https://github.com/github/spec-kit).
## Available Extensions
| Extension | Purpose | URL |
|-----------|---------|-----|
| 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) |
| 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) |
## Adding Your Extension
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for instructions on how to submit your extension to the community catalog.

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +0,0 @@
{
"schema_version": "1.0",
"updated_at": "2026-02-24T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"cleanup": {
"name": "Cleanup Extension",
"id": "cleanup",
"description": "Post-implementation quality gate that reviews changes, fixes small issues (scout rule), creates tasks for medium issues, and generates analysis for large issues.",
"author": "dsrednicki",
"version": "1.0.0",
"download_url": "https://github.com/dsrednicki/spec-kit-cleanup/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/dsrednicki/spec-kit-cleanup",
"homepage": "https://github.com/dsrednicki/spec-kit-cleanup",
"documentation": "https://github.com/dsrednicki/spec-kit-cleanup/blob/main/README.md",
"changelog": "https://github.com/dsrednicki/spec-kit-cleanup/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": ["quality", "tech-debt", "review", "cleanup", "scout-rule"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-22T00:00:00Z",
"updated_at": "2026-02-22T00:00:00Z"
},
"retrospective": {
"name": "Retrospective Extension",
"id": "retrospective",
"description": "Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates.",
"author": "emi-dm",
"version": "1.0.0",
"download_url": "https://github.com/emi-dm/spec-kit-retrospective/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/emi-dm/spec-kit-retrospective",
"homepage": "https://github.com/emi-dm/spec-kit-retrospective",
"documentation": "https://github.com/emi-dm/spec-kit-retrospective/blob/main/README.md",
"changelog": "https://github.com/emi-dm/spec-kit-retrospective/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": ["retrospective", "spec-drift", "quality", "analysis", "governance"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-24T00:00:00Z",
"updated_at": "2026-02-24T00:00:00Z"
},
"v-model": {
"name": "V-Model Extension Pack",
"id": "v-model",
"description": "Enforces V-Model paired generation of development specs and test specs with full traceability.",
"author": "leocamello",
"version": "0.4.0",
"download_url": "https://github.com/leocamello/spec-kit-v-model/archive/refs/tags/v0.4.0.zip",
"repository": "https://github.com/leocamello/spec-kit-v-model",
"homepage": "https://github.com/leocamello/spec-kit-v-model",
"documentation": "https://github.com/leocamello/spec-kit-v-model/blob/main/README.md",
"changelog": "https://github.com/leocamello/spec-kit-v-model/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 9,
"hooks": 1
},
"tags": ["v-model", "traceability", "testing", "compliance", "safety-critical"],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-02-20T00:00:00Z",
"updated_at": "2026-02-22T00:00:00Z"
}
}
}

View File

@@ -1,6 +0,0 @@
{
"schema_version": "1.0",
"updated_at": "2026-02-03T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
"extensions": {}
}

View File

@@ -1,39 +0,0 @@
# Local configuration overrides
*-config.local.yml
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
# Testing
.pytest_cache/
.coverage
htmlcov/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Logs
*.log
# Build artifacts
dist/
build/
*.egg-info/
# Temporary files
*.tmp
.cache/

View File

@@ -1,39 +0,0 @@
# Changelog
All notable changes to this extension will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Planned
- Feature ideas for future versions
- Enhancements
- Bug fixes
## [1.0.0] - YYYY-MM-DD
### Added
- Initial release of extension
- Command: `/speckit.my-extension.example` - Example command functionality
- Configuration system with template
- Documentation and examples
### Features
- Feature 1 description
- Feature 2 description
- Feature 3 description
### Requirements
- Spec Kit: >=0.1.0
- External dependencies (if any)
---
[Unreleased]: https://github.com/your-org/spec-kit-my-extension/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/your-org/spec-kit-my-extension/releases/tag/v1.0.0

View File

@@ -1,158 +0,0 @@
# EXAMPLE: Extension README
This is an example of what your extension README should look like after customization.
**Delete this file and replace README.md with content similar to this.**
---
# My Extension
<!-- CUSTOMIZE: Replace with your extension description -->
Brief description of what your extension does and why it's useful.
## Features
<!-- CUSTOMIZE: List key features -->
- Feature 1: Description
- Feature 2: Description
- Feature 3: Description
## Installation
```bash
# Install from catalog
specify extension add my-extension
# Or install from local development directory
specify extension add --dev /path/to/my-extension
```
## Configuration
1. Create configuration file:
```bash
cp .specify/extensions/my-extension/config-template.yml \
.specify/extensions/my-extension/my-extension-config.yml
```
2. Edit configuration:
```bash
vim .specify/extensions/my-extension/my-extension-config.yml
```
3. Set required values:
<!-- CUSTOMIZE: List required configuration -->
```yaml
connection:
url: "https://api.example.com"
api_key: "your-api-key"
project:
id: "your-project-id"
```
## Usage
<!-- CUSTOMIZE: Add usage examples -->
### Command: example
Description of what this command does.
```bash
# In Claude Code
> /speckit.my-extension.example
```
**Prerequisites**:
- Prerequisite 1
- Prerequisite 2
**Output**:
- What this command produces
- Where results are saved
## Configuration Reference
<!-- CUSTOMIZE: Document all configuration options -->
### Connection Settings
| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `connection.url` | string | Yes | API endpoint URL |
| `connection.api_key` | string | Yes | API authentication key |
### Project Settings
| Setting | Type | Required | Description |
|---------|------|----------|-------------|
| `project.id` | string | Yes | Project identifier |
| `project.workspace` | string | No | Workspace or organization |
## Environment Variables
Override configuration with environment variables:
```bash
# Override connection settings
export SPECKIT_MY_EXTENSION_CONNECTION_URL="https://custom-api.com"
export SPECKIT_MY_EXTENSION_CONNECTION_API_KEY="custom-key"
```
## Examples
<!-- CUSTOMIZE: Add real-world examples -->
### Example 1: Basic Workflow
```bash
# Step 1: Create specification
> /speckit.spec
# Step 2: Generate tasks
> /speckit.tasks
# Step 3: Use extension
> /speckit.my-extension.example
```
## Troubleshooting
<!-- CUSTOMIZE: Add common issues -->
### Issue: Configuration not found
**Solution**: Create config from template (see Configuration section)
### Issue: Command not available
**Solutions**:
1. Check extension is installed: `specify extension list`
2. Restart AI agent
3. Reinstall extension
## License
MIT License - see LICENSE file
## Support
- **Issues**: <https://github.com/your-org/spec-kit-my-extension/issues>
- **Spec Kit Docs**: <https://github.com/statsperform/spec-kit>
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history.
---
*Extension Version: 1.0.0*
*Spec Kit: >=0.1.0*

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 [Your Name or Organization]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,79 +0,0 @@
# Extension Template
Starter template for creating a Spec Kit extension.
## Quick Start
1. **Copy this template**:
```bash
cp -r extensions/template my-extension
cd my-extension
```
2. **Customize `extension.yml`**:
- Change extension ID, name, description
- Update author and repository
- Define your commands
3. **Create commands**:
- Add command files in `commands/` directory
- Use Markdown format with YAML frontmatter
4. **Create config template**:
- Define configuration options
- Document all settings
5. **Write documentation**:
- Update README.md with usage instructions
- Add examples
6. **Test locally**:
```bash
cd /path/to/spec-kit-project
specify extension add --dev /path/to/my-extension
```
7. **Publish** (optional):
- Create GitHub repository
- Create release
- Submit to catalog (see EXTENSION-PUBLISHING-GUIDE.md)
## Files in This Template
- `extension.yml` - Extension manifest (CUSTOMIZE THIS)
- `config-template.yml` - Configuration template (CUSTOMIZE THIS)
- `commands/example.md` - Example command (REPLACE THIS)
- `README.md` - Extension documentation (REPLACE THIS)
- `LICENSE` - MIT License (REVIEW THIS)
- `CHANGELOG.md` - Version history (UPDATE THIS)
- `.gitignore` - Git ignore rules
## Customization Checklist
- [ ] Update `extension.yml` with your extension details
- [ ] Change extension ID to your extension name
- [ ] Update author information
- [ ] Define your commands
- [ ] Create command files in `commands/`
- [ ] Update config template
- [ ] Write README with usage instructions
- [ ] Add examples
- [ ] Update LICENSE if needed
- [ ] Test extension locally
- [ ] Create git repository
- [ ] Create first release
## Need Help?
- **Development Guide**: See EXTENSION-DEVELOPMENT-GUIDE.md
- **API Reference**: See EXTENSION-API-REFERENCE.md
- **Publishing Guide**: See EXTENSION-PUBLISHING-GUIDE.md
- **User Guide**: See EXTENSION-USER-GUIDE.md
## Template Version
- Version: 1.0.0
- Last Updated: 2026-01-28
- Compatible with Spec Kit: >=0.1.0

View File

@@ -1,210 +0,0 @@
---
description: "Example command that demonstrates extension functionality"
# CUSTOMIZE: List MCP tools this command uses
tools:
- 'example-mcp-server/example_tool'
---
# Example Command
<!-- CUSTOMIZE: Replace this entire file with your command documentation -->
This is an example command that demonstrates how to create commands for Spec Kit extensions.
## Purpose
Describe what this command does and when to use it.
## Prerequisites
List requirements before using this command:
1. Prerequisite 1 (e.g., "MCP server configured")
2. Prerequisite 2 (e.g., "Configuration file exists")
3. Prerequisite 3 (e.g., "Valid API credentials")
## User Input
$ARGUMENTS
## Steps
### Step 1: Load Configuration
<!-- CUSTOMIZE: Replace with your actual steps -->
Load extension configuration from the project:
``bash
config_file=".specify/extensions/my-extension/my-extension-config.yml"
if [ ! -f "$config_file" ]; then
echo "❌ Error: Configuration not found at $config_file"
echo "Run 'specify extension add my-extension' to install and configure"
exit 1
fi
# Read configuration values
setting_value=$(yq eval '.settings.key' "$config_file")
# Apply environment variable overrides
setting_value="${SPECKIT_MY_EXTENSION_KEY:-$setting_value}"
# Validate configuration
if [ -z "$setting_value" ]; then
echo "❌ Error: Configuration value not set"
echo "Edit $config_file and set 'settings.key'"
exit 1
fi
echo "📋 Configuration loaded: $setting_value"
``
### Step 2: Perform Main Action
<!-- CUSTOMIZE: Replace with your command logic -->
Describe what this step does:
``markdown
Use MCP tools to perform the main action:
- Tool: example-mcp-server example_tool
- Parameters: { "key": "$setting_value" }
This calls the MCP server tool to execute the operation.
``
### Step 3: Process Results
<!-- CUSTOMIZE: Add more steps as needed -->
Process the results and provide output:
`` bash
echo ""
echo "✅ Command completed successfully!"
echo ""
echo "Results:"
echo " • Item 1: Value"
echo " • Item 2: Value"
echo ""
``
### Step 4: Save Output (Optional)
Save results to a file if needed:
``bash
output_file=".specify/my-extension-output.json"
cat > "$output_file" <<EOF
{
"timestamp": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
"setting": "$setting_value",
"results": []
}
EOF
echo "💾 Output saved to $output_file"
``
## Configuration Reference
<!-- CUSTOMIZE: Document configuration options -->
This command uses the following configuration from `my-extension-config.yml`:
- **settings.key**: Description of what this setting does
- Type: string
- Required: Yes
- Example: `"example-value"`
- **settings.another_key**: Description of another setting
- Type: boolean
- Required: No
- Default: `false`
- Example: `true`
## Environment Variables
<!-- CUSTOMIZE: Document environment variable overrides -->
Configuration can be overridden with environment variables:
- `SPECKIT_MY_EXTENSION_KEY` - Overrides `settings.key`
- `SPECKIT_MY_EXTENSION_ANOTHER_KEY` - Overrides `settings.another_key`
Example:
``bash
export SPECKIT_MY_EXTENSION_KEY="override-value"
``
## Troubleshooting
<!-- CUSTOMIZE: Add common issues and solutions -->
### "Configuration not found"
**Solution**: Install the extension and create configuration:
``bash
specify extension add my-extension
cp .specify/extensions/my-extension/config-template.yml \
.specify/extensions/my-extension/my-extension-config.yml
``
### "MCP tool not available"
**Solution**: Ensure MCP server is configured in your AI agent settings.
### "Permission denied"
**Solution**: Check credentials and permissions in the external service.
## Notes
<!-- CUSTOMIZE: Add helpful notes and tips -->
- This command requires an active connection to the external service
- Results are cached for performance
- Re-run the command to refresh data
## Examples
<!-- CUSTOMIZE: Add usage examples -->
### Example 1: Basic Usage
``bash
# Run with default configuration
>
> /speckit.my-extension.example
``
### Example 2: With Environment Override
``bash
# Override configuration with environment variable
export SPECKIT_MY_EXTENSION_KEY="custom-value"
> /speckit.my-extension.example
``
### Example 3: After Core Command
``bash
# Use as part of a workflow
>
> /speckit.tasks
> /speckit.my-extension.example
``
---
*For more information, see the extension README or run `specify extension info my-extension`*

View File

@@ -1,75 +0,0 @@
# Extension Configuration Template
# Copy this to my-extension-config.yml and customize for your project
# CUSTOMIZE: Add your configuration sections below
# Example: Connection settings
connection:
# URL to external service
url: "" # REQUIRED: e.g., "https://api.example.com"
# API key or token
api_key: "" # REQUIRED: Your API key
# Example: Project settings
project:
# Project identifier
id: "" # REQUIRED: e.g., "my-project"
# Workspace or organization
workspace: "" # OPTIONAL: e.g., "my-org"
# Example: Feature flags
features:
# Enable/disable main functionality
enabled: true
# Automatic synchronization
auto_sync: false
# Verbose logging
verbose: false
# Example: Default values
defaults:
# Labels to apply
labels: [] # e.g., ["automated", "spec-kit"]
# Priority level
priority: "medium" # Options: "low", "medium", "high"
# Assignee
assignee: "" # OPTIONAL: Default assignee
# Example: Field mappings
# Map internal names to external field IDs
field_mappings:
# Example mappings
# internal_field: "external_field_id"
# status: "customfield_10001"
# Example: Advanced settings
advanced:
# Timeout in seconds
timeout: 30
# Retry attempts
retry_count: 3
# Cache duration in seconds
cache_duration: 3600
# Environment Variable Overrides:
# You can override any setting with environment variables using this pattern:
# SPECKIT_MY_EXTENSION_{SECTION}_{KEY}
#
# Examples:
# - SPECKIT_MY_EXTENSION_CONNECTION_API_KEY: Override connection.api_key
# - SPECKIT_MY_EXTENSION_PROJECT_ID: Override project.id
# - SPECKIT_MY_EXTENSION_FEATURES_ENABLED: Override features.enabled
#
# Note: Use uppercase and replace dots with underscores
# Local Overrides:
# For local development, create my-extension-config.local.yml (gitignored)
# to override settings without affecting the team configuration

View File

@@ -1,97 +0,0 @@
schema_version: "1.0"
extension:
# CUSTOMIZE: Change 'my-extension' to your extension ID (lowercase, hyphen-separated)
id: "my-extension"
# CUSTOMIZE: Human-readable name for your extension
name: "My Extension"
# 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 extension does"
# CUSTOMIZE: Your name or organization name
author: "Your Name"
# CUSTOMIZE: GitHub repository URL (create before publishing)
repository: "https://github.com/your-org/spec-kit-my-extension"
# REVIEW: License (MIT is recommended for open source)
license: "MIT"
# CUSTOMIZE: Extension homepage (can be same as repository)
homepage: "https://github.com/your-org/spec-kit-my-extension"
# Requirements for this extension
requires:
# CUSTOMIZE: Minimum spec-kit version required
# Use >=X.Y.Z for minimum version
# Use >=X.Y.Z,<Y.0.0 for version range
speckit_version: ">=0.1.0"
# CUSTOMIZE: Add MCP tools or other dependencies
# Remove if no external tools required
tools:
- name: "example-mcp-server"
version: ">=1.0.0"
required: true
# Commands provided by this extension
provides:
commands:
# CUSTOMIZE: Define your commands
# Pattern: speckit.{extension-id}.{command-name}
- name: "speckit.my-extension.example"
file: "commands/example.md"
description: "Example command that demonstrates functionality"
# Optional: Add aliases for shorter command names
aliases: ["speckit.example"]
# ADD MORE COMMANDS: Copy this block for each command
# - name: "speckit.my-extension.another-command"
# file: "commands/another-command.md"
# description: "Another command"
# CUSTOMIZE: Define configuration files
config:
- name: "my-extension-config.yml"
template: "config-template.yml"
description: "Extension configuration"
required: true # Set to false if config is optional
# CUSTOMIZE: Define hooks (optional)
# Remove if no hooks needed
hooks:
# Hook that runs after /speckit.tasks
after_tasks:
command: "speckit.my-extension.example"
optional: true # User will be prompted
prompt: "Run example command?"
description: "Demonstrates hook functionality"
condition: null # Future: conditional execution
# ADD MORE HOOKS: Copy this block for other events
# after_implement:
# command: "speckit.my-extension.another"
# optional: false # Auto-execute without prompting
# description: "Runs automatically after implementation"
# CUSTOMIZE: Add relevant tags (2-5 recommended)
# Used for discovery in catalog
tags:
- "example"
- "template"
# ADD MORE: "category", "tool-name", etc.
# CUSTOMIZE: Default configuration values (optional)
# These are merged with user config
defaults:
# Example default values
feature:
enabled: true
auto_sync: false
# ADD MORE: Any default settings for your extension

View File

@@ -47,4 +47,4 @@
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->

View File

@@ -0,0 +1,85 @@
# Constitution Update Checklist
When amending the constitution (`/memory/constitution.md`), ensure all dependent documents are updated to maintain consistency.
## Templates to Update
### When adding/modifying ANY article:
- [ ] `/templates/plan-template.md` - Update Constitution Check section
- [ ] `/templates/spec-template.md` - Update if requirements/scope affected
- [ ] `/templates/tasks-template.md` - Update if new task types needed
- [ ] `/.claude/commands/plan.md` - Update if planning process changes
- [ ] `/.claude/commands/tasks.md` - Update if task generation affected
- [ ] `/CLAUDE.md` - Update runtime development guidelines
### Article-specific updates:
#### Article I (Library-First):
- [ ] Ensure templates emphasize library creation
- [ ] Update CLI command examples
- [ ] Add llms.txt documentation requirements
#### Article II (CLI Interface):
- [ ] Update CLI flag requirements in templates
- [ ] Add text I/O protocol reminders
#### Article III (Test-First):
- [ ] Update test order in all templates
- [ ] Emphasize TDD requirements
- [ ] Add test approval gates
#### Article IV (Integration Testing):
- [ ] List integration test triggers
- [ ] Update test type priorities
- [ ] Add real dependency requirements
#### Article V (Observability):
- [ ] Add logging requirements to templates
- [ ] Include multi-tier log streaming
- [ ] Update performance monitoring sections
#### Article VI (Versioning):
- [ ] Add version increment reminders
- [ ] Include breaking change procedures
- [ ] Update migration requirements
#### Article VII (Simplicity):
- [ ] Update project count limits
- [ ] Add pattern prohibition examples
- [ ] Include YAGNI reminders
## Validation Steps
1. **Before committing constitution changes:**
- [ ] All templates reference new requirements
- [ ] Examples updated to match new rules
- [ ] No contradictions between documents
2. **After updating templates:**
- [ ] Run through a sample implementation plan
- [ ] Verify all constitution requirements addressed
- [ ] Check that templates are self-contained (readable without constitution)
3. **Version tracking:**
- [ ] Update constitution version number
- [ ] Note version in template footers
- [ ] Add amendment to constitution history
## Common Misses
Watch for these often-forgotten updates:
- Command documentation (`/commands/*.md`)
- Checklist items in templates
- Example code/commands
- Domain-specific variations (web vs mobile vs CLI)
- Cross-references between documents
## Template Sync Status
Last sync check: 2025-07-16
- Constitution version: 2.1.1
- Templates aligned: ❌ (missing versioning, observability details)
---
*This checklist ensures the constitution's principles are consistently applied across all project documentation.*

View File

@@ -1,54 +1,44 @@
[project]
name = "specify-cli"
version = "0.1.6"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
version = "0.0.4"
description = "Setup tool for Specify spec-driven development projects"
requires-python = ">=3.11"
dependencies = [
# Existing spec-kit dependencies
"typer",
"click>=8.1",
"rich",
"rich>=13.0.0",
"httpx[socks]",
"platformdirs",
"readchar",
"truststore>=0.10.4",
"pyyaml>=6.0",
"packaging>=23.0",
# APM dependencies (from awd-cli, excluding runtime/embargo items)
"click>=8.0.0",
"colorama>=0.4.6",
"pyyaml>=6.0.0",
"requests>=2.28.0",
"python-frontmatter>=1.0.0",
"tomli>=1.2.0; python_version<'3.11'",
"toml>=0.10.2",
"rich-click>=1.7.0",
"watchdog>=3.0.0",
"GitPython>=3.1.0",
]
[project.scripts]
specify = "specify_cli:main"
[project.optional-dependencies]
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=23.0.0",
"isort>=5.0.0",
"mypy>=1.0.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/specify_cli"]
[project.optional-dependencies]
test = [
"pytest>=7.0",
"pytest-cov>=4.0",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--tb=short",
]
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/__pycache__/*"]
[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
packages = ["src/specify_cli", "src/apm_cli"]

View File

@@ -1,166 +0,0 @@
#!/usr/bin/env bash
# Consolidated prerequisite checking script
#
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
# It replaces the functionality previously spread across multiple scripts.
#
# Usage: ./check-prerequisites.sh [OPTIONS]
#
# OPTIONS:
# --json Output in JSON format
# --require-tasks Require tasks.md to exist (for implementation phase)
# --include-tasks Include tasks.md in AVAILABLE_DOCS list
# --paths-only Only output path variables (no validation)
# --help, -h Show help message
#
# OUTPUTS:
# JSON mode: {"FEATURE_DIR":"...", "AVAILABLE_DOCS":["..."]}
# Text mode: FEATURE_DIR:... \n AVAILABLE_DOCS: \n ✓/✗ file.md
# Paths only: REPO_ROOT: ... \n BRANCH: ... \n FEATURE_DIR: ... etc.
set -e
# Parse command line arguments
JSON_MODE=false
REQUIRE_TASKS=false
INCLUDE_TASKS=false
PATHS_ONLY=false
for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--require-tasks)
REQUIRE_TASKS=true
;;
--include-tasks)
INCLUDE_TASKS=true
;;
--paths-only)
PATHS_ONLY=true
;;
--help|-h)
cat << 'EOF'
Usage: check-prerequisites.sh [OPTIONS]
Consolidated prerequisite checking for Spec-Driven Development workflow.
OPTIONS:
--json Output in JSON format
--require-tasks Require tasks.md to exist (for implementation phase)
--include-tasks Include tasks.md in AVAILABLE_DOCS list
--paths-only Only output path variables (no prerequisite validation)
--help, -h Show this help message
EXAMPLES:
# Check task prerequisites (plan.md required)
./check-prerequisites.sh --json
# Check implementation prerequisites (plan.md + tasks.md required)
./check-prerequisites.sh --json --require-tasks --include-tasks
# Get feature paths only (no validation)
./check-prerequisites.sh --paths-only
EOF
exit 0
;;
*)
echo "ERROR: Unknown option '$arg'. Use --help for usage information." >&2
exit 1
;;
esac
done
# Source common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get feature paths and validate branch
eval $(get_feature_paths)
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; then
if $JSON_MODE; then
# Minimal JSON paths payload (no validation performed)
printf '{"REPO_ROOT":"%s","BRANCH":"%s","FEATURE_DIR":"%s","FEATURE_SPEC":"%s","IMPL_PLAN":"%s","TASKS":"%s"}\n' \
"$REPO_ROOT" "$CURRENT_BRANCH" "$FEATURE_DIR" "$FEATURE_SPEC" "$IMPL_PLAN" "$TASKS"
else
echo "REPO_ROOT: $REPO_ROOT"
echo "BRANCH: $CURRENT_BRANCH"
echo "FEATURE_DIR: $FEATURE_DIR"
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "TASKS: $TASKS"
fi
exit 0
fi
# Validate required directories and files
if [[ ! -d "$FEATURE_DIR" ]]; then
echo "ERROR: Feature directory not found: $FEATURE_DIR" >&2
echo "Run /speckit.specify first to create the feature structure." >&2
exit 1
fi
if [[ ! -f "$IMPL_PLAN" ]]; then
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
echo "Run /speckit.plan first to create the implementation plan." >&2
exit 1
fi
# Check for tasks.md if required
if $REQUIRE_TASKS && [[ ! -f "$TASKS" ]]; then
echo "ERROR: tasks.md not found in $FEATURE_DIR" >&2
echo "Run /speckit.tasks first to create the task list." >&2
exit 1
fi
# Build list of available documents
docs=()
# Always check these optional docs
[[ -f "$RESEARCH" ]] && docs+=("research.md")
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
# Check contracts directory (only if it exists and has files)
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
docs+=("contracts/")
fi
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
# Include tasks.md if requested and it exists
if $INCLUDE_TASKS && [[ -f "$TASKS" ]]; then
docs+=("tasks.md")
fi
# Output results
if $JSON_MODE; then
# Build JSON array of documents
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(printf '"%s",' "${docs[@]}")
json_docs="[${json_docs%,}]"
fi
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs"
else
# Text output
echo "FEATURE_DIR:$FEATURE_DIR"
echo "AVAILABLE_DOCS:"
# Show status of each potential document
check_file "$RESEARCH" "research.md"
check_file "$DATA_MODEL" "data-model.md"
check_dir "$CONTRACTS_DIR" "contracts/"
check_file "$QUICKSTART" "quickstart.md"
if $INCLUDE_TASKS; then
check_file "$TASKS" "tasks.md"
fi
fi

View File

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

View File

@@ -1,145 +1,27 @@
#!/usr/bin/env bash
# Common functions and variables for all scripts
# (Moved to scripts/bash/) Common functions and variables for all scripts
# Get repository root, with fallback for non-git repositories
get_repo_root() {
if git rev-parse --show-toplevel >/dev/null 2>&1; then
git rev-parse --show-toplevel
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
}
# Get current branch, with fallback for non-git repositories
get_current_branch() {
# First check if SPECIFY_FEATURE environment variable is set
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
echo "$SPECIFY_FEATURE"
return
fi
# Then check git if available
if git rev-parse --abbrev-ref HEAD >/dev/null 2>&1; then
git rev-parse --abbrev-ref HEAD
return
fi
# For non-git repos, try to find the latest feature directory
local repo_root=$(get_repo_root)
local specs_dir="$repo_root/specs"
if [[ -d "$specs_dir" ]]; then
local latest_feature=""
local highest=0
for dir in "$specs_dir"/*; do
if [[ -d "$dir" ]]; then
local dirname=$(basename "$dir")
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
local number=${BASH_REMATCH[1]}
number=$((10#$number))
if [[ "$number" -gt "$highest" ]]; then
highest=$number
latest_feature=$dirname
fi
fi
fi
done
if [[ -n "$latest_feature" ]]; then
echo "$latest_feature"
return
fi
fi
echo "main" # Final fallback
}
# Check if we have git available
has_git() {
git rev-parse --show-toplevel >/dev/null 2>&1
}
get_repo_root() { git rev-parse --show-toplevel; }
get_current_branch() { git rev-parse --abbrev-ref HEAD; }
check_feature_branch() {
local branch="$1"
local has_git_repo="$2"
# For non-git repos, we can't enforce branch naming but still provide output
if [[ "$has_git_repo" != "true" ]]; then
echo "[specify] Warning: Git repository not detected; skipped branch validation" >&2
return 0
fi
if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then
echo "ERROR: Not on a feature branch. Current branch: $branch" >&2
echo "Feature branches should be named like: 001-feature-name" >&2
return 1
fi
return 0
fi; return 0
}
get_feature_dir() { echo "$1/specs/$2"; }
# Find feature directory by numeric prefix instead of exact branch match
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
find_feature_dir_by_prefix() {
local repo_root="$1"
local branch_name="$2"
local specs_dir="$repo_root/specs"
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
# If branch doesn't have numeric prefix, fall back to exact match
echo "$specs_dir/$branch_name"
return
fi
local prefix="${BASH_REMATCH[1]}"
# Search for directories in specs/ that start with this prefix
local matches=()
if [[ -d "$specs_dir" ]]; then
for dir in "$specs_dir"/"$prefix"-*; do
if [[ -d "$dir" ]]; then
matches+=("$(basename "$dir")")
fi
done
fi
# Handle results
if [[ ${#matches[@]} -eq 0 ]]; then
# No match found - return the branch name path (will fail later with clear error)
echo "$specs_dir/$branch_name"
elif [[ ${#matches[@]} -eq 1 ]]; then
# Exactly one match - perfect!
echo "$specs_dir/${matches[0]}"
else
# Multiple matches - this shouldn't happen with proper naming convention
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
echo "Please ensure only one spec directory exists per numeric prefix." >&2
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
fi
}
get_feature_paths() {
local repo_root=$(get_repo_root)
local current_branch=$(get_current_branch)
local has_git_repo="false"
if has_git; then
has_git_repo="true"
fi
# Use prefix-based lookup to support multiple branches per spec
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
local feature_dir=$(get_feature_dir "$repo_root" "$current_branch")
cat <<EOF
REPO_ROOT='$repo_root'
CURRENT_BRANCH='$current_branch'
HAS_GIT='$has_git_repo'
FEATURE_DIR='$feature_dir'
FEATURE_SPEC='$feature_dir/spec.md'
IMPL_PLAN='$feature_dir/plan.md'
@@ -153,4 +35,3 @@ EOF
check_file() { [[ -f "$1" ]] && echo "$2" || echo "$2"; }
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo "$2" || echo "$2"; }

View File

@@ -1,297 +1,58 @@
#!/usr/bin/env bash
# (Moved to scripts/bash/) Create a new feature with branch, directory structure, and template
set -e
JSON_MODE=false
SHORT_NAME=""
BRANCH_NUMBER=""
ARGS=()
i=1
while [ $i -le $# ]; do
arg="${!i}"
for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--short-name)
if [ $((i + 1)) -gt $# ]; then
echo 'Error: --short-name requires a value' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
# Check if the next argument is another option (starts with --)
if [[ "$next_arg" == --* ]]; then
echo 'Error: --short-name requires a value' >&2
exit 1
fi
SHORT_NAME="$next_arg"
;;
--number)
if [ $((i + 1)) -gt $# ]; then
echo 'Error: --number requires a value' >&2
exit 1
fi
i=$((i + 1))
next_arg="${!i}"
if [[ "$next_arg" == --* ]]; then
echo 'Error: --number requires a value' >&2
exit 1
fi
BRANCH_NUMBER="$next_arg"
;;
--help|-h)
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
echo ""
echo "Options:"
echo " --json Output in JSON format"
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 " --help, -h Show this help message"
echo ""
echo "Examples:"
echo " $0 'Add user authentication system' --short-name 'user-auth'"
echo " $0 'Implement OAuth2 integration for API' --number 5"
exit 0
;;
*)
ARGS+=("$arg")
;;
--json) JSON_MODE=true ;;
--help|-h) echo "Usage: $0 [--json] <feature_description>"; exit 0 ;;
*) ARGS+=("$arg") ;;
esac
i=$((i + 1))
done
FEATURE_DESCRIPTION="${ARGS[*]}"
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
echo "Usage: $0 [--json] <feature_description>" >&2
exit 1
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
get_highest_from_specs() {
local specs_dir="$1"
local highest=0
if [ -d "$specs_dir" ]; then
for dir in "$specs_dir"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$highest" ]; then
highest=$number
fi
done
fi
echo "$highest"
}
# Function to get highest number from git branches
get_highest_from_branches() {
local highest=0
# Get all branches (local and remote)
branches=$(git branch -a 2>/dev/null || echo "")
if [ -n "$branches" ]; then
while IFS= read -r branch; do
# 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
done <<< "$branches"
fi
echo "$highest"
}
# Function to check existing branches (local and remote) and return next available number
check_existing_branches() {
local specs_dir="$1"
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
git fetch --all --prune 2>/dev/null || true
# Get highest number from ALL branches (not just matching short name)
local highest_branch=$(get_highest_from_branches)
# Get highest number from ALL specs (not just matching short name)
local highest_spec=$(get_highest_from_specs "$specs_dir")
# Take the maximum of both
local max_num=$highest_branch
if [ "$highest_spec" -gt "$max_num" ]; then
max_num=$highest_spec
fi
# Return next number
echo $((max_num + 1))
}
# Function to clean and format a branch name
clean_branch_name() {
local name="$1"
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
}
# 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)"
if git rev-parse --show-toplevel >/dev/null 2>&1; then
REPO_ROOT=$(git rev-parse --show-toplevel)
HAS_GIT=true
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
fi
cd "$REPO_ROOT"
REPO_ROOT=$(git rev-parse --show-toplevel)
SPECS_DIR="$REPO_ROOT/specs"
mkdir -p "$SPECS_DIR"
# Function to generate branch name with stop word filtering and length filtering
generate_branch_name() {
local description="$1"
# Common stop words to filter out
local stop_words="^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$"
# Convert to lowercase and split into words
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
local meaningful_words=()
for word in $clean_name; do
# Skip empty words
[ -z "$word" ] && continue
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
if ! echo "$word" | grep -qiE "$stop_words"; then
if [ ${#word} -ge 3 ]; then
meaningful_words+=("$word")
elif echo "$description" | grep -q "\b${word^^}\b"; then
# Keep short words if they appear as uppercase in original (likely acronyms)
meaningful_words+=("$word")
fi
fi
HIGHEST=0
if [ -d "$SPECS_DIR" ]; then
for dir in "$SPECS_DIR"/*; do
[ -d "$dir" ] || continue
dirname=$(basename "$dir")
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
number=$((10#$number))
if [ "$number" -gt "$HIGHEST" ]; then HIGHEST=$number; fi
done
# If we have meaningful words, use first 3-4 of them
if [ ${#meaningful_words[@]} -gt 0 ]; then
local max_words=3
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
local result=""
local count=0
for word in "${meaningful_words[@]}"; do
if [ $count -ge $max_words ]; then break; fi
if [ -n "$result" ]; then result="$result-"; fi
result="$result$word"
count=$((count + 1))
done
echo "$result"
else
# Fallback to original logic if no meaningful words found
local cleaned=$(clean_branch_name "$description")
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
fi
}
# Generate branch name
if [ -n "$SHORT_NAME" ]; then
# Use provided short name, just clean it up
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
else
# Generate from description with smart filtering
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
fi
# Determine branch number
if [ -z "$BRANCH_NUMBER" ]; then
if [ "$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
NEXT=$((HIGHEST + 1))
FEATURE_NUM=$(printf "%03d" "$NEXT")
# 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}"
BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//')
WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//')
BRANCH_NAME="${FEATURE_NUM}-${WORDS}"
# GitHub enforces a 244-byte limit on branch names
# Validate and truncate if necessary
MAX_BRANCH_LENGTH=244
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
# Calculate how much we need to trim from suffix
# Account for: feature number (3) + hyphen (1) = 4 chars
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - 4))
# Truncate suffix at word boundary if possible
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
# Remove trailing hyphen if truncation created one
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
fi
if [ "$HAS_GIT" = true ]; then
git checkout -b "$BRANCH_NAME"
else
>&2 echo "[specify] Warning: Git repository not detected; skipped branch creation for $BRANCH_NAME"
fi
git checkout -b "$BRANCH_NAME"
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
mkdir -p "$FEATURE_DIR"
TEMPLATE="$REPO_ROOT/.specify/templates/spec-template.md"
TEMPLATE="$REPO_ROOT/templates/spec-template.md"
SPEC_FILE="$FEATURE_DIR/spec.md"
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
# Set the SPECIFY_FEATURE environment variable for the current session
export SPECIFY_FEATURE="$BRANCH_NAME"
if $JSON_MODE; then
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
else
echo "BRANCH_NAME: $BRANCH_NAME"
echo "SPEC_FILE: $SPEC_FILE"
echo "FEATURE_NUM: $FEATURE_NUM"
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
fi

View File

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

View File

@@ -1,61 +1,17 @@
#!/usr/bin/env bash
set -e
# Parse command line arguments
JSON_MODE=false
ARGS=()
for arg in "$@"; do
case "$arg" in
--json)
JSON_MODE=true
;;
--help|-h)
echo "Usage: $0 [--json]"
echo " --json Output results in JSON format"
echo " --help Show this help message"
exit 0
;;
*)
ARGS+=("$arg")
;;
esac
done
# Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
for arg in "$@"; do case "$arg" in --json) JSON_MODE=true ;; --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; esac; done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
eval $(get_feature_paths)
# Check if we're on a proper feature branch (only for git repos)
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
# Ensure the feature directory exists
check_feature_branch "$CURRENT_BRANCH" || exit 1
mkdir -p "$FEATURE_DIR"
# Copy plan template if it exists
TEMPLATE="$REPO_ROOT/.specify/templates/plan-template.md"
if [[ -f "$TEMPLATE" ]]; then
cp "$TEMPLATE" "$IMPL_PLAN"
echo "Copied plan template to $IMPL_PLAN"
else
echo "Warning: Plan template not found at $TEMPLATE"
# Create a basic plan file if template doesn't exist
touch "$IMPL_PLAN"
fi
# Output results
[[ -f "$TEMPLATE" ]] && cp "$TEMPLATE" "$IMPL_PLAN"
if $JSON_MODE; then
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s","HAS_GIT":"%s"}\n' \
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" "$HAS_GIT"
printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \
"$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH"
else
echo "FEATURE_SPEC: $FEATURE_SPEC"
echo "IMPL_PLAN: $IMPL_PLAN"
echo "SPECS_DIR: $FEATURE_DIR"
echo "BRANCH: $CURRENT_BRANCH"
echo "HAS_GIT: $HAS_GIT"
echo "FEATURE_SPEC: $FEATURE_SPEC"; echo "IMPL_PLAN: $IMPL_PLAN"; echo "SPECS_DIR: $FEATURE_DIR"; echo "BRANCH: $CURRENT_BRANCH"
fi

View File

@@ -1,810 +1,57 @@
#!/usr/bin/env bash
# Update agent context files with information from plan.md
#
# This script maintains AI agent context files by parsing feature specifications
# and updating agent-specific configuration files with project information.
#
# MAIN FUNCTIONS:
# 1. Environment Validation
# - Verifies git repository structure and branch information
# - Checks for required plan.md files and templates
# - Validates file permissions and accessibility
#
# 2. Plan Data Extraction
# - Parses plan.md files to extract project metadata
# - Identifies language/version, frameworks, databases, and project types
# - Handles missing or incomplete specification data gracefully
#
# 3. Agent File Management
# - Creates new agent context files from templates when needed
# - Updates existing agent files with new project information
# - Preserves manual additions and custom configurations
# - Supports multiple AI agent formats and directory structures
#
# 4. Content Generation
# - Generates language-specific build/test commands
# - Creates appropriate project directory structures
# - Updates technology stacks and recent changes sections
# - Maintains consistent formatting and timestamps
#
# 5. Multi-Agent Support
# - Handles agent-specific file paths and naming conventions
# - Supports: Claude, Gemini, Copilot, Cursor, Qwen, opencode, Codex, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy CLI, Qoder CLI, Amp, SHAI, Amazon Q Developer CLI, or Antigravity
# - Can update single agents or all existing agent files
# - Creates default Claude file if no agent files exist
#
# Usage: ./update-agent-context.sh [agent_type]
# Agent types: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli
# Leave empty to update all existing agent files
set -e
# Enable strict error handling
set -u
set -o pipefail
#==============================================================================
# Configuration and Global Variables
#==============================================================================
# Get script directory and load common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get all paths and variables from common functions
eval $(get_feature_paths)
NEW_PLAN="$IMPL_PLAN" # Alias for compatibility with existing code
AGENT_TYPE="${1:-}"
# Agent-specific file paths
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"
GEMINI_FILE="$REPO_ROOT/GEMINI.md"
COPILOT_FILE="$REPO_ROOT/.github/agents/copilot-instructions.md"
CURSOR_FILE="$REPO_ROOT/.cursor/rules/specify-rules.mdc"
QWEN_FILE="$REPO_ROOT/QWEN.md"
AGENTS_FILE="$REPO_ROOT/AGENTS.md"
WINDSURF_FILE="$REPO_ROOT/.windsurf/rules/specify-rules.md"
KILOCODE_FILE="$REPO_ROOT/.kilocode/rules/specify-rules.md"
AUGGIE_FILE="$REPO_ROOT/.augment/rules/specify-rules.md"
ROO_FILE="$REPO_ROOT/.roo/rules/specify-rules.md"
CODEBUDDY_FILE="$REPO_ROOT/CODEBUDDY.md"
QODER_FILE="$REPO_ROOT/QODER.md"
AMP_FILE="$REPO_ROOT/AGENTS.md"
SHAI_FILE="$REPO_ROOT/SHAI.md"
Q_FILE="$REPO_ROOT/AGENTS.md"
AGY_FILE="$REPO_ROOT/.agent/rules/specify-rules.md"
BOB_FILE="$REPO_ROOT/AGENTS.md"
# Template file
TEMPLATE_FILE="$REPO_ROOT/.specify/templates/agent-file-template.md"
# Global variables for parsed plan data
NEW_LANG=""
NEW_FRAMEWORK=""
NEW_DB=""
NEW_PROJECT_TYPE=""
#==============================================================================
# Utility Functions
#==============================================================================
log_info() {
echo "INFO: $1"
}
log_success() {
echo "$1"
}
log_error() {
echo "ERROR: $1" >&2
}
log_warning() {
echo "WARNING: $1" >&2
}
# Cleanup function for temporary files
cleanup() {
local exit_code=$?
rm -f /tmp/agent_update_*_$$
rm -f /tmp/manual_additions_$$
exit $exit_code
}
# Set up cleanup trap
trap cleanup EXIT INT TERM
#==============================================================================
# Validation Functions
#==============================================================================
validate_environment() {
# Check if we have a current branch/feature (git or non-git)
if [[ -z "$CURRENT_BRANCH" ]]; then
log_error "Unable to determine current feature"
if [[ "$HAS_GIT" == "true" ]]; then
log_info "Make sure you're on a feature branch"
else
log_info "Set SPECIFY_FEATURE environment variable or create a feature first"
fi
exit 1
fi
# Check if plan.md exists
if [[ ! -f "$NEW_PLAN" ]]; then
log_error "No plan.md found at $NEW_PLAN"
log_info "Make sure you're working on a feature with a corresponding spec directory"
if [[ "$HAS_GIT" != "true" ]]; then
log_info "Use: export SPECIFY_FEATURE=your-feature-name or create a new feature first"
fi
exit 1
fi
# Check if template exists (needed for new files)
if [[ ! -f "$TEMPLATE_FILE" ]]; then
log_warning "Template file not found at $TEMPLATE_FILE"
log_warning "Creating new agent files will fail"
fi
}
#==============================================================================
# Plan Parsing Functions
#==============================================================================
extract_plan_field() {
local field_pattern="$1"
local plan_file="$2"
grep "^\*\*${field_pattern}\*\*: " "$plan_file" 2>/dev/null | \
head -1 | \
sed "s|^\*\*${field_pattern}\*\*: ||" | \
sed 's/^[ \t]*//;s/[ \t]*$//' | \
grep -v "NEEDS CLARIFICATION" | \
grep -v "^N/A$" || echo ""
}
parse_plan_data() {
local plan_file="$1"
if [[ ! -f "$plan_file" ]]; then
log_error "Plan file not found: $plan_file"
return 1
fi
if [[ ! -r "$plan_file" ]]; then
log_error "Plan file is not readable: $plan_file"
return 1
fi
log_info "Parsing plan data from $plan_file"
NEW_LANG=$(extract_plan_field "Language/Version" "$plan_file")
NEW_FRAMEWORK=$(extract_plan_field "Primary Dependencies" "$plan_file")
NEW_DB=$(extract_plan_field "Storage" "$plan_file")
NEW_PROJECT_TYPE=$(extract_plan_field "Project Type" "$plan_file")
# Log what we found
if [[ -n "$NEW_LANG" ]]; then
log_info "Found language: $NEW_LANG"
else
log_warning "No language information found in plan"
fi
if [[ -n "$NEW_FRAMEWORK" ]]; then
log_info "Found framework: $NEW_FRAMEWORK"
fi
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
log_info "Found database: $NEW_DB"
fi
if [[ -n "$NEW_PROJECT_TYPE" ]]; then
log_info "Found project type: $NEW_PROJECT_TYPE"
fi
}
format_technology_stack() {
local lang="$1"
local framework="$2"
local parts=()
# Add non-empty parts
[[ -n "$lang" && "$lang" != "NEEDS CLARIFICATION" ]] && parts+=("$lang")
[[ -n "$framework" && "$framework" != "NEEDS CLARIFICATION" && "$framework" != "N/A" ]] && parts+=("$framework")
# Join with proper formatting
if [[ ${#parts[@]} -eq 0 ]]; then
echo ""
elif [[ ${#parts[@]} -eq 1 ]]; then
echo "${parts[0]}"
else
# Join multiple parts with " + "
local result="${parts[0]}"
for ((i=1; i<${#parts[@]}; i++)); do
result="$result + ${parts[i]}"
done
echo "$result"
fi
}
#==============================================================================
# Template and Content Generation Functions
#==============================================================================
get_project_structure() {
local project_type="$1"
if [[ "$project_type" == *"web"* ]]; then
echo "backend/\\nfrontend/\\ntests/"
else
echo "src/\\ntests/"
fi
}
get_commands_for_language() {
local lang="$1"
case "$lang" in
*"Python"*)
echo "cd src && pytest && ruff check ."
;;
*"Rust"*)
echo "cargo test && cargo clippy"
;;
*"JavaScript"*|*"TypeScript"*)
echo "npm test \\&\\& npm run lint"
;;
*)
echo "# Add commands for $lang"
;;
esac
}
get_language_conventions() {
local lang="$1"
echo "$lang: Follow standard conventions"
}
create_new_agent_file() {
local target_file="$1"
local temp_file="$2"
local project_name="$3"
local current_date="$4"
if [[ ! -f "$TEMPLATE_FILE" ]]; then
log_error "Template not found at $TEMPLATE_FILE"
return 1
fi
if [[ ! -r "$TEMPLATE_FILE" ]]; then
log_error "Template file is not readable: $TEMPLATE_FILE"
return 1
fi
log_info "Creating new agent context file from template..."
if ! cp "$TEMPLATE_FILE" "$temp_file"; then
log_error "Failed to copy template file"
return 1
fi
# Replace template placeholders
local project_structure
project_structure=$(get_project_structure "$NEW_PROJECT_TYPE")
local commands
commands=$(get_commands_for_language "$NEW_LANG")
local language_conventions
language_conventions=$(get_language_conventions "$NEW_LANG")
# Perform substitutions with error checking using safer approach
# Escape special characters for sed by using a different delimiter or escaping
local escaped_lang=$(printf '%s\n' "$NEW_LANG" | sed 's/[\[\.*^$()+{}|]/\\&/g')
local escaped_framework=$(printf '%s\n' "$NEW_FRAMEWORK" | sed 's/[\[\.*^$()+{}|]/\\&/g')
local escaped_branch=$(printf '%s\n' "$CURRENT_BRANCH" | sed 's/[\[\.*^$()+{}|]/\\&/g')
# Build technology stack and recent change strings conditionally
local tech_stack
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
tech_stack="- $escaped_lang + $escaped_framework ($escaped_branch)"
elif [[ -n "$escaped_lang" ]]; then
tech_stack="- $escaped_lang ($escaped_branch)"
elif [[ -n "$escaped_framework" ]]; then
tech_stack="- $escaped_framework ($escaped_branch)"
else
tech_stack="- ($escaped_branch)"
fi
local recent_change
if [[ -n "$escaped_lang" && -n "$escaped_framework" ]]; then
recent_change="- $escaped_branch: Added $escaped_lang + $escaped_framework"
elif [[ -n "$escaped_lang" ]]; then
recent_change="- $escaped_branch: Added $escaped_lang"
elif [[ -n "$escaped_framework" ]]; then
recent_change="- $escaped_branch: Added $escaped_framework"
else
recent_change="- $escaped_branch: Added"
fi
local substitutions=(
"s|\[PROJECT NAME\]|$project_name|"
"s|\[DATE\]|$current_date|"
"s|\[EXTRACTED FROM ALL PLAN.MD FILES\]|$tech_stack|"
"s|\[ACTUAL STRUCTURE FROM PLANS\]|$project_structure|g"
"s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$commands|"
"s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$language_conventions|"
"s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|$recent_change|"
)
for substitution in "${substitutions[@]}"; do
if ! sed -i.bak -e "$substitution" "$temp_file"; then
log_error "Failed to perform substitution: $substitution"
rm -f "$temp_file" "$temp_file.bak"
return 1
fi
done
# Convert \n sequences to actual newlines
newline=$(printf '\n')
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
# Clean up backup files
rm -f "$temp_file.bak" "$temp_file.bak2"
return 0
}
update_existing_agent_file() {
local target_file="$1"
local current_date="$2"
log_info "Updating existing agent context file..."
# Use a single temporary file for atomic update
local temp_file
temp_file=$(mktemp) || {
log_error "Failed to create temporary file"
return 1
}
# Process the file in one pass
local tech_stack=$(format_technology_stack "$NEW_LANG" "$NEW_FRAMEWORK")
local new_tech_entries=()
local new_change_entry=""
# Prepare new technology entries
if [[ -n "$tech_stack" ]] && ! grep -q "$tech_stack" "$target_file"; then
new_tech_entries+=("- $tech_stack ($CURRENT_BRANCH)")
fi
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]] && ! grep -q "$NEW_DB" "$target_file"; then
new_tech_entries+=("- $NEW_DB ($CURRENT_BRANCH)")
fi
# Prepare new change entry
if [[ -n "$tech_stack" ]]; then
new_change_entry="- $CURRENT_BRANCH: Added $tech_stack"
elif [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]] && [[ "$NEW_DB" != "NEEDS CLARIFICATION" ]]; then
new_change_entry="- $CURRENT_BRANCH: Added $NEW_DB"
fi
# Check if sections exist in the file
local has_active_technologies=0
local has_recent_changes=0
if grep -q "^## Active Technologies" "$target_file" 2>/dev/null; then
has_active_technologies=1
fi
if grep -q "^## Recent Changes" "$target_file" 2>/dev/null; then
has_recent_changes=1
fi
# Process file line by line
local in_tech_section=false
local in_changes_section=false
local tech_entries_added=false
local changes_entries_added=false
local existing_changes_count=0
local file_ended=false
while IFS= read -r line || [[ -n "$line" ]]; do
# Handle Active Technologies section
if [[ "$line" == "## Active Technologies" ]]; then
echo "$line" >> "$temp_file"
in_tech_section=true
continue
elif [[ $in_tech_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
# Add new tech entries before closing the section
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
echo "$line" >> "$temp_file"
in_tech_section=false
continue
elif [[ $in_tech_section == true ]] && [[ -z "$line" ]]; then
# Add new tech entries before empty line in tech section
if [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
echo "$line" >> "$temp_file"
continue
fi
# Handle Recent Changes section
if [[ "$line" == "## Recent Changes" ]]; then
echo "$line" >> "$temp_file"
# Add new change entry right after the heading
if [[ -n "$new_change_entry" ]]; then
echo "$new_change_entry" >> "$temp_file"
fi
in_changes_section=true
changes_entries_added=true
continue
elif [[ $in_changes_section == true ]] && [[ "$line" =~ ^##[[:space:]] ]]; then
echo "$line" >> "$temp_file"
in_changes_section=false
continue
elif [[ $in_changes_section == true ]] && [[ "$line" == "- "* ]]; then
# Keep only first 2 existing changes
if [[ $existing_changes_count -lt 2 ]]; then
echo "$line" >> "$temp_file"
((existing_changes_count++))
fi
continue
fi
# Update timestamp
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"
else
echo "$line" >> "$temp_file"
fi
done < "$target_file"
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
if [[ $in_tech_section == true ]] && [[ $tech_entries_added == false ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
# If sections don't exist, add them at the end of the file
if [[ $has_active_technologies -eq 0 ]] && [[ ${#new_tech_entries[@]} -gt 0 ]]; then
echo "" >> "$temp_file"
echo "## Active Technologies" >> "$temp_file"
printf '%s\n' "${new_tech_entries[@]}" >> "$temp_file"
tech_entries_added=true
fi
if [[ $has_recent_changes -eq 0 ]] && [[ -n "$new_change_entry" ]]; then
echo "" >> "$temp_file"
echo "## Recent Changes" >> "$temp_file"
echo "$new_change_entry" >> "$temp_file"
changes_entries_added=true
fi
# Move temp file to target atomically
if ! mv "$temp_file" "$target_file"; then
log_error "Failed to update target file"
rm -f "$temp_file"
return 1
fi
return 0
}
#==============================================================================
# Main Agent File Update Function
#==============================================================================
update_agent_file() {
local target_file="$1"
local agent_name="$2"
if [[ -z "$target_file" ]] || [[ -z "$agent_name" ]]; then
log_error "update_agent_file requires target_file and agent_name parameters"
return 1
fi
log_info "Updating $agent_name context file: $target_file"
local project_name
project_name=$(basename "$REPO_ROOT")
local current_date
current_date=$(date +%Y-%m-%d)
# Create directory if it doesn't exist
local target_dir
target_dir=$(dirname "$target_file")
if [[ ! -d "$target_dir" ]]; then
if ! mkdir -p "$target_dir"; then
log_error "Failed to create directory: $target_dir"
return 1
fi
fi
if [[ ! -f "$target_file" ]]; then
# Create new file from template
local temp_file
temp_file=$(mktemp) || {
log_error "Failed to create temporary file"
return 1
}
if create_new_agent_file "$target_file" "$temp_file" "$project_name" "$current_date"; then
if mv "$temp_file" "$target_file"; then
log_success "Created new $agent_name context file"
else
log_error "Failed to move temporary file to $target_file"
rm -f "$temp_file"
return 1
fi
else
log_error "Failed to create new agent file"
rm -f "$temp_file"
return 1
fi
else
# Update existing file
if [[ ! -r "$target_file" ]]; then
log_error "Cannot read existing file: $target_file"
return 1
fi
if [[ ! -w "$target_file" ]]; then
log_error "Cannot write to existing file: $target_file"
return 1
fi
if update_existing_agent_file "$target_file" "$current_date"; then
log_success "Updated existing $agent_name context file"
else
log_error "Failed to update existing agent file"
return 1
fi
fi
return 0
}
#==============================================================================
# Agent Selection and Processing
#==============================================================================
update_specific_agent() {
local agent_type="$1"
case "$agent_type" in
claude)
update_agent_file "$CLAUDE_FILE" "Claude Code"
;;
gemini)
update_agent_file "$GEMINI_FILE" "Gemini CLI"
;;
copilot)
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
;;
cursor-agent)
update_agent_file "$CURSOR_FILE" "Cursor IDE"
;;
qwen)
update_agent_file "$QWEN_FILE" "Qwen Code"
;;
opencode)
update_agent_file "$AGENTS_FILE" "opencode"
;;
codex)
update_agent_file "$AGENTS_FILE" "Codex CLI"
;;
windsurf)
update_agent_file "$WINDSURF_FILE" "Windsurf"
;;
kilocode)
update_agent_file "$KILOCODE_FILE" "Kilo Code"
;;
auggie)
update_agent_file "$AUGGIE_FILE" "Auggie CLI"
;;
roo)
update_agent_file "$ROO_FILE" "Roo Code"
;;
codebuddy)
update_agent_file "$CODEBUDDY_FILE" "CodeBuddy CLI"
;;
qodercli)
update_agent_file "$QODER_FILE" "Qoder CLI"
;;
amp)
update_agent_file "$AMP_FILE" "Amp"
;;
shai)
update_agent_file "$SHAI_FILE" "SHAI"
;;
q)
update_agent_file "$Q_FILE" "Amazon Q Developer CLI"
;;
agy)
update_agent_file "$AGY_FILE" "Antigravity"
;;
bob)
update_agent_file "$BOB_FILE" "IBM Bob"
;;
generic)
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 "Expected: claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic"
exit 1
;;
esac
}
update_all_existing_agents() {
local found_agent=false
# Check each possible agent file and update if it exists
if [[ -f "$CLAUDE_FILE" ]]; then
update_agent_file "$CLAUDE_FILE" "Claude Code"
found_agent=true
fi
if [[ -f "$GEMINI_FILE" ]]; then
update_agent_file "$GEMINI_FILE" "Gemini CLI"
found_agent=true
fi
if [[ -f "$COPILOT_FILE" ]]; then
update_agent_file "$COPILOT_FILE" "GitHub Copilot"
found_agent=true
fi
if [[ -f "$CURSOR_FILE" ]]; then
update_agent_file "$CURSOR_FILE" "Cursor IDE"
found_agent=true
fi
if [[ -f "$QWEN_FILE" ]]; then
update_agent_file "$QWEN_FILE" "Qwen Code"
found_agent=true
fi
if [[ -f "$AGENTS_FILE" ]]; then
update_agent_file "$AGENTS_FILE" "Codex/opencode"
found_agent=true
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
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 "$Q_FILE" ]]; then
update_agent_file "$Q_FILE" "Amazon Q Developer 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() {
echo
log_info "Summary of changes:"
if [[ -n "$NEW_LANG" ]]; then
echo " - Added language: $NEW_LANG"
fi
if [[ -n "$NEW_FRAMEWORK" ]]; then
echo " - Added framework: $NEW_FRAMEWORK"
fi
if [[ -n "$NEW_DB" ]] && [[ "$NEW_DB" != "N/A" ]]; then
echo " - Added database: $NEW_DB"
fi
echo
log_info "Usage: $0 [claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli]"
}
#==============================================================================
# Main Execution
#==============================================================================
main() {
# Validate environment before proceeding
validate_environment
log_info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
# Parse the plan file to extract project information
if ! parse_plan_data "$NEW_PLAN"; then
log_error "Failed to parse plan data"
exit 1
fi
# Process based on agent type argument
local success=true
if [[ -z "$AGENT_TYPE" ]]; then
# No specific agent provided - update all existing agent files
log_info "No agent specified, updating all existing agent files..."
if ! update_all_existing_agents; then
success=false
fi
else
# Specific agent provided - update only that agent
log_info "Updating specific agent: $AGENT_TYPE"
if ! update_specific_agent "$AGENT_TYPE"; then
success=false
fi
fi
# Print summary
print_summary
if [[ "$success" == true ]]; then
log_success "Agent context update completed successfully"
exit 0
else
log_error "Agent context update completed with errors"
exit 1
fi
}
# Execute main function if script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
REPO_ROOT=$(git rev-parse --show-toplevel)
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH"
NEW_PLAN="$FEATURE_DIR/plan.md"
CLAUDE_FILE="$REPO_ROOT/CLAUDE.md"; GEMINI_FILE="$REPO_ROOT/GEMINI.md"; COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md"
AGENT_TYPE="$1"
[ -f "$NEW_PLAN" ] || { echo "ERROR: No plan.md found at $NEW_PLAN"; exit 1; }
echo "=== Updating agent context files for feature $CURRENT_BRANCH ==="
NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "")
NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "")
update_agent_file() { local target_file="$1" agent_name="$2"; echo "Updating $agent_name context file: $target_file"; local temp_file=$(mktemp); if [ ! -f "$target_file" ]; then
echo "Creating new $agent_name context file..."; if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file"; else echo "ERROR: Template not found"; return 1; fi;
sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file"; sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file"; sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file";
if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file"; else sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file"; fi;
if [[ "$NEW_LANG" == *"Python"* ]]; then COMMANDS="cd src && pytest && ruff check ."; elif [[ "$NEW_LANG" == *"Rust"* ]]; then COMMANDS="cargo test && cargo clippy"; elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then COMMANDS="npm test && npm run lint"; else COMMANDS="# Add commands for $NEW_LANG"; fi; sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file";
sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file"; sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file"; rm "$temp_file.bak";
else
echo "Updating existing $agent_name context file..."; manual_start=$(grep -n "<!-- MANUAL ADDITIONS START -->" "$target_file" | cut -d: -f1); manual_end=$(grep -n "<!-- MANUAL ADDITIONS END -->" "$target_file" | cut -d: -f1); if [ -n "$manual_start" ] && [ -n "$manual_end" ]; then sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt; fi;
python3 - "$target_file" <<'EOF'
import re,sys,datetime
target=sys.argv[1]
with open(target) as f: content=f.read()
NEW_LANG="'$NEW_LANG'";NEW_FRAMEWORK="'$NEW_FRAMEWORK'";CURRENT_BRANCH="'$CURRENT_BRANCH'";NEW_DB="'$NEW_DB'";NEW_PROJECT_TYPE="'$NEW_PROJECT_TYPE'"
# Tech section
m=re.search(r'## Active Technologies\n(.*?)\n\n',content, re.DOTALL)
if m:
existing=m.group(1)
additions=[]
if '$NEW_LANG' and '$NEW_LANG' not in existing: additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)")
if '$NEW_DB' and '$NEW_DB' not in existing and '$NEW_DB'!='N/A': additions.append(f"- $NEW_DB ($CURRENT_BRANCH)")
if additions:
new_block=existing+"\n"+"\n".join(additions)
content=content.replace(m.group(0),f"## Active Technologies\n{new_block}\n\n")
# Recent changes
m2=re.search(r'## Recent Changes\n(.*?)(\n\n|$)',content, re.DOTALL)
if m2:
lines=[l for l in m2.group(1).strip().split('\n') if l]
lines.insert(0,f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK")
lines=lines[:3]
content=re.sub(r'## Recent Changes\n.*?(\n\n|$)', '## Recent Changes\n'+"\n".join(lines)+'\n\n', content, flags=re.DOTALL)
content=re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', 'Last updated: '+datetime.datetime.now().strftime('%Y-%m-%d'), content)
open(target+'.tmp','w').write(content)
EOF
mv "$target_file.tmp" "$target_file"; if [ -f /tmp/manual_additions.txt ]; then sed -i.bak '/<!-- MANUAL ADDITIONS START -->/,/<!-- MANUAL ADDITIONS END -->/d' "$target_file"; cat /tmp/manual_additions.txt >> "$target_file"; rm /tmp/manual_additions.txt "$target_file.bak"; fi;
fi; mv "$temp_file" "$target_file" 2>/dev/null || true; echo "$agent_name context file updated successfully"; }
case "$AGENT_TYPE" in
claude) update_agent_file "$CLAUDE_FILE" "Claude Code" ;;
gemini) update_agent_file "$GEMINI_FILE" "Gemini CLI" ;;
copilot) update_agent_file "$COPILOT_FILE" "GitHub Copilot" ;;
"") [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code"; [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI"; [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot"; if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then update_agent_file "$CLAUDE_FILE" "Claude Code"; fi ;;
*) echo "ERROR: Unknown agent type '$AGENT_TYPE'"; exit 1 ;;
esac
echo; echo "Summary of changes:"; [ -n "$NEW_LANG" ] && echo "- Added language: $NEW_LANG"; [ -n "$NEW_FRAMEWORK" ] && echo "- Added framework: $NEW_FRAMEWORK"; [ -n "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ] && echo "- Added database: $NEW_DB"; echo; echo "Usage: $0 [claude|gemini|copilot]"

View File

@@ -1,148 +0,0 @@
#!/usr/bin/env pwsh
# Consolidated prerequisite checking script (PowerShell)
#
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
# It replaces the functionality previously spread across multiple scripts.
#
# Usage: ./check-prerequisites.ps1 [OPTIONS]
#
# OPTIONS:
# -Json Output in JSON format
# -RequireTasks Require tasks.md to exist (for implementation phase)
# -IncludeTasks Include tasks.md in AVAILABLE_DOCS list
# -PathsOnly Only output path variables (no validation)
# -Help, -h Show help message
[CmdletBinding()]
param(
[switch]$Json,
[switch]$RequireTasks,
[switch]$IncludeTasks,
[switch]$PathsOnly,
[switch]$Help
)
$ErrorActionPreference = 'Stop'
# Show help if requested
if ($Help) {
Write-Output @"
Usage: check-prerequisites.ps1 [OPTIONS]
Consolidated prerequisite checking for Spec-Driven Development workflow.
OPTIONS:
-Json Output in JSON format
-RequireTasks Require tasks.md to exist (for implementation phase)
-IncludeTasks Include tasks.md in AVAILABLE_DOCS list
-PathsOnly Only output path variables (no prerequisite validation)
-Help, -h Show this help message
EXAMPLES:
# Check task prerequisites (plan.md required)
.\check-prerequisites.ps1 -Json
# Check implementation prerequisites (plan.md + tasks.md required)
.\check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
# Get feature paths only (no validation)
.\check-prerequisites.ps1 -PathsOnly
"@
exit 0
}
# Source common functions
. "$PSScriptRoot/common.ps1"
# Get feature paths and validate branch
$paths = Get-FeaturePathsEnv
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) {
exit 1
}
# If paths-only mode, output paths and exit (support combined -Json -PathsOnly)
if ($PathsOnly) {
if ($Json) {
[PSCustomObject]@{
REPO_ROOT = $paths.REPO_ROOT
BRANCH = $paths.CURRENT_BRANCH
FEATURE_DIR = $paths.FEATURE_DIR
FEATURE_SPEC = $paths.FEATURE_SPEC
IMPL_PLAN = $paths.IMPL_PLAN
TASKS = $paths.TASKS
} | ConvertTo-Json -Compress
} else {
Write-Output "REPO_ROOT: $($paths.REPO_ROOT)"
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
Write-Output "TASKS: $($paths.TASKS)"
}
exit 0
}
# Validate required directories and files
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
Write-Output "Run /speckit.specify first to create the feature structure."
exit 1
}
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
Write-Output "Run /speckit.plan first to create the implementation plan."
exit 1
}
# Check for tasks.md if required
if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) {
Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)"
Write-Output "Run /speckit.tasks first to create the task list."
exit 1
}
# Build list of available documents
$docs = @()
# Always check these optional docs
if (Test-Path $paths.RESEARCH) { $docs += 'research.md' }
if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' }
# Check contracts directory (only if it exists and has files)
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) {
$docs += 'contracts/'
}
if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' }
# Include tasks.md if requested and it exists
if ($IncludeTasks -and (Test-Path $paths.TASKS)) {
$docs += 'tasks.md'
}
# Output results
if ($Json) {
# JSON output
[PSCustomObject]@{
FEATURE_DIR = $paths.FEATURE_DIR
AVAILABLE_DOCS = $docs
} | ConvertTo-Json -Compress
} else {
# Text output
Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)"
Write-Output "AVAILABLE_DOCS:"
# Show status of each potential document
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
if ($IncludeTasks) {
Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Out-Null
}
}

View File

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

View File

@@ -1,84 +1,16 @@
#!/usr/bin/env pwsh
# Common PowerShell functions analogous to common.sh
# Common PowerShell functions analogous to common.sh (moved to powershell/)
function Get-RepoRoot {
try {
$result = git rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -eq 0) {
return $result
}
} catch {
# Git command failed
}
# Fall back to script location for non-git repos
return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path
git rev-parse --show-toplevel
}
function Get-CurrentBranch {
# First check if SPECIFY_FEATURE environment variable is set
if ($env:SPECIFY_FEATURE) {
return $env:SPECIFY_FEATURE
}
# Then check git if available
try {
$result = git rev-parse --abbrev-ref HEAD 2>$null
if ($LASTEXITCODE -eq 0) {
return $result
}
} catch {
# Git command failed
}
# For non-git repos, try to find the latest feature directory
$repoRoot = Get-RepoRoot
$specsDir = Join-Path $repoRoot "specs"
if (Test-Path $specsDir) {
$latestFeature = ""
$highest = 0
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})-') {
$num = [int]$matches[1]
if ($num -gt $highest) {
$highest = $num
$latestFeature = $_.Name
}
}
}
if ($latestFeature) {
return $latestFeature
}
}
# Final fallback
return "main"
}
function Test-HasGit {
try {
git rev-parse --show-toplevel 2>$null | Out-Null
return ($LASTEXITCODE -eq 0)
} catch {
return $false
}
git rev-parse --abbrev-ref HEAD
}
function Test-FeatureBranch {
param(
[string]$Branch,
[bool]$HasGit = $true
)
# For non-git repos, we can't enforce branch naming but still provide output
if (-not $HasGit) {
Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation"
return $true
}
param([string]$Branch)
if ($Branch -notmatch '^[0-9]{3}-') {
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
Write-Output "Feature branches should be named like: 001-feature-name"
@@ -95,20 +27,17 @@ function Get-FeatureDir {
function Get-FeaturePathsEnv {
$repoRoot = Get-RepoRoot
$currentBranch = Get-CurrentBranch
$hasGit = Test-HasGit
$featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch
[PSCustomObject]@{
REPO_ROOT = $repoRoot
REPO_ROOT = $repoRoot
CURRENT_BRANCH = $currentBranch
HAS_GIT = $hasGit
FEATURE_DIR = $featureDir
FEATURE_SPEC = Join-Path $featureDir 'spec.md'
IMPL_PLAN = Join-Path $featureDir 'plan.md'
TASKS = Join-Path $featureDir 'tasks.md'
RESEARCH = Join-Path $featureDir 'research.md'
DATA_MODEL = Join-Path $featureDir 'data-model.md'
QUICKSTART = Join-Path $featureDir 'quickstart.md'
FEATURE_DIR = $featureDir
FEATURE_SPEC = Join-Path $featureDir 'spec.md'
IMPL_PLAN = Join-Path $featureDir 'plan.md'
TASKS = Join-Path $featureDir 'tasks.md'
RESEARCH = Join-Path $featureDir 'research.md'
DATA_MODEL = Join-Path $featureDir 'data-model.md'
QUICKSTART = Join-Path $featureDir 'quickstart.md'
CONTRACTS_DIR = Join-Path $featureDir 'contracts'
}
}
@@ -134,4 +63,3 @@ function Test-DirHasFiles {
return $false
}
}

View File

@@ -1,283 +1,52 @@
#!/usr/bin/env pwsh
# Create a new feature
# Create a new feature (moved to powershell/)
[CmdletBinding()]
param(
[switch]$Json,
[string]$ShortName,
[int]$Number = 0,
[switch]$Help,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$FeatureDescription
)
$ErrorActionPreference = 'Stop'
# Show help if requested
if ($Help) {
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
Write-Host ""
Write-Host "Options:"
Write-Host " -Json Output in JSON format"
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
Write-Host " -Help Show this help message"
Write-Host ""
Write-Host "Examples:"
Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'"
Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'"
exit 0
}
# Check if feature description provided
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
exit 1
Write-Error "Usage: ./create-new-feature.ps1 [-Json] <feature description>"; exit 1
}
$featureDesc = ($FeatureDescription -join ' ').Trim()
# Resolve repository root. Prefer git information when available, but fall back
# to searching for repository markers so the workflow still functions in repositories that
# were initialized with --no-git.
function Find-RepositoryRoot {
param(
[string]$StartDir,
[string[]]$Markers = @('.git', '.specify')
)
$current = Resolve-Path $StartDir
while ($true) {
foreach ($marker in $Markers) {
if (Test-Path (Join-Path $current $marker)) {
return $current
}
}
$parent = Split-Path $current -Parent
if ($parent -eq $current) {
# Reached filesystem root without finding markers
return $null
}
$current = $parent
}
}
function Get-HighestNumberFromSpecs {
param([string]$SpecsDir)
$highest = 0
if (Test-Path $SpecsDir) {
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d+)') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
return $highest
}
function Get-HighestNumberFromBranches {
param()
$highest = 0
try {
$branches = git branch -a 2>$null
if ($LASTEXITCODE -eq 0) {
foreach ($branch in $branches) {
# Clean branch name: remove leading markers and remote prefixes
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
# Extract feature number if branch matches pattern ###-*
if ($cleanBranch -match '^(\d+)-') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
}
} catch {
# If git command fails, return 0
Write-Verbose "Could not check Git branches: $_"
}
return $highest
}
function Get-NextBranchNumber {
param(
[string]$SpecsDir
)
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
try {
git fetch --all --prune 2>$null | Out-Null
} catch {
# Ignore fetch errors
}
# Get highest number from ALL branches (not just matching short name)
$highestBranch = Get-HighestNumberFromBranches
# Get highest number from ALL specs (not just matching short name)
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
# Take the maximum of both
$maxNum = [Math]::Max($highestBranch, $highestSpec)
# Return next number
return $maxNum + 1
}
function ConvertTo-CleanBranchName {
param([string]$Name)
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
}
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
if (-not $fallbackRoot) {
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
exit 1
}
try {
$repoRoot = git rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -eq 0) {
$hasGit = $true
} else {
throw "Git not available"
}
} catch {
$repoRoot = $fallbackRoot
$hasGit = $false
}
Set-Location $repoRoot
$repoRoot = git rev-parse --show-toplevel
$specsDir = Join-Path $repoRoot 'specs'
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
# Function to generate branch name with stop word filtering and length filtering
function Get-BranchName {
param([string]$Description)
# Common stop words to filter out
$stopWords = @(
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must', 'shall',
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
'want', 'need', 'add', 'get', 'set'
)
# Convert to lowercase and extract words (alphanumeric only)
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
$words = $cleanName -split '\s+' | Where-Object { $_ }
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
$meaningfulWords = @()
foreach ($word in $words) {
# Skip stop words
if ($stopWords -contains $word) { continue }
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
if ($word.Length -ge 3) {
$meaningfulWords += $word
} elseif ($Description -match "\b$($word.ToUpper())\b") {
# Keep short words if they appear as uppercase in original (likely acronyms)
$meaningfulWords += $word
$highest = 0
if (Test-Path $specsDir) {
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
if ($_.Name -match '^(\d{3})') {
$num = [int]$matches[1]
if ($num -gt $highest) { $highest = $num }
}
}
# If we have meaningful words, use first 3-4 of them
if ($meaningfulWords.Count -gt 0) {
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
$result = ($meaningfulWords | Select-Object -First $maxWords) -join '-'
return $result
} else {
# Fallback to original logic if no meaningful words found
$result = ConvertTo-CleanBranchName -Name $Description
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
return [string]::Join('-', $fallbackWords)
}
}
$next = $highest + 1
$featureNum = ('{0:000}' -f $next)
# Generate branch name
if ($ShortName) {
# Use provided short name, just clean it up
$branchSuffix = ConvertTo-CleanBranchName -Name $ShortName
} else {
# Generate from description with smart filtering
$branchSuffix = Get-BranchName -Description $featureDesc
}
$branchName = $featureDesc.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
$words = ($branchName -split '-') | Where-Object { $_ } | Select-Object -First 3
$branchName = "$featureNum-$([string]::Join('-', $words))"
# Determine branch number
if ($Number -eq 0) {
if ($hasGit) {
# Check existing branches on remotes
$Number = Get-NextBranchNumber -SpecsDir $specsDir
} else {
# Fall back to local directory check
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
}
}
$featureNum = ('{0:000}' -f $Number)
$branchName = "$featureNum-$branchSuffix"
# GitHub enforces a 244-byte limit on branch names
# Validate and truncate if necessary
$maxBranchLength = 244
if ($branchName.Length -gt $maxBranchLength) {
# Calculate how much we need to trim from suffix
# Account for: feature number (3) + hyphen (1) = 4 chars
$maxSuffixLength = $maxBranchLength - 4
# Truncate suffix
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
# Remove trailing hyphen if truncation created one
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
$originalBranchName = $branchName
$branchName = "$featureNum-$truncatedSuffix"
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
}
if ($hasGit) {
try {
git checkout -b $branchName | Out-Null
} catch {
Write-Warning "Failed to create git branch: $branchName"
}
} else {
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
}
git checkout -b $branchName | Out-Null
$featureDir = Join-Path $specsDir $branchName
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
$template = Join-Path $repoRoot '.specify/templates/spec-template.md'
$template = Join-Path $repoRoot 'templates/spec-template.md'
$specFile = Join-Path $featureDir 'spec.md'
if (Test-Path $template) {
Copy-Item $template $specFile -Force
} else {
New-Item -ItemType File -Path $specFile | Out-Null
}
# Set the SPECIFY_FEATURE environment variable for the current session
$env:SPECIFY_FEATURE = $branchName
if (Test-Path $template) { Copy-Item $template $specFile -Force } else { New-Item -ItemType File -Path $specFile | Out-Null }
if ($Json) {
$obj = [PSCustomObject]@{
BRANCH_NAME = $branchName
SPEC_FILE = $specFile
FEATURE_NUM = $featureNum
HAS_GIT = $hasGit
}
$obj = [PSCustomObject]@{ BRANCH_NAME = $branchName; SPEC_FILE = $specFile; FEATURE_NUM = $featureNum }
$obj | ConvertTo-Json -Compress
} else {
Write-Output "BRANCH_NAME: $branchName"
Write-Output "SPEC_FILE: $specFile"
Write-Output "FEATURE_NUM: $featureNum"
Write-Output "HAS_GIT: $hasGit"
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
}

View File

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

View File

@@ -1,61 +1,21 @@
#!/usr/bin/env pwsh
# Setup implementation plan for a feature
[CmdletBinding()]
param(
[switch]$Json,
[switch]$Help
)
param([switch]$Json)
$ErrorActionPreference = 'Stop'
# Show help if requested
if ($Help) {
Write-Output "Usage: ./setup-plan.ps1 [-Json] [-Help]"
Write-Output " -Json Output results in JSON format"
Write-Output " -Help Show this help message"
exit 0
}
# Load common functions
. "$PSScriptRoot/common.ps1"
# Get all paths and variables from common functions
$paths = Get-FeaturePathsEnv
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH)) { exit 1 }
# Check if we're on a proper feature branch (only for git repos)
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
exit 1
}
# Ensure the feature directory exists
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
$template = Join-Path $paths.REPO_ROOT 'templates/plan-template.md'
if (Test-Path $template) { Copy-Item $template $paths.IMPL_PLAN -Force }
# Copy plan template if it exists, otherwise note it or create empty file
$template = Join-Path $paths.REPO_ROOT '.specify/templates/plan-template.md'
if (Test-Path $template) {
Copy-Item $template $paths.IMPL_PLAN -Force
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
} else {
Write-Warning "Plan template not found at $template"
# Create a basic plan file if template doesn't exist
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
}
# Output results
if ($Json) {
$result = [PSCustomObject]@{
FEATURE_SPEC = $paths.FEATURE_SPEC
IMPL_PLAN = $paths.IMPL_PLAN
SPECS_DIR = $paths.FEATURE_DIR
BRANCH = $paths.CURRENT_BRANCH
HAS_GIT = $paths.HAS_GIT
}
$result | ConvertTo-Json -Compress
[PSCustomObject]@{ FEATURE_SPEC=$paths.FEATURE_SPEC; IMPL_PLAN=$paths.IMPL_PLAN; SPECS_DIR=$paths.FEATURE_DIR; BRANCH=$paths.CURRENT_BRANCH } | ConvertTo-Json -Compress
} else {
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)"
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
Write-Output "HAS_GIT: $($paths.HAS_GIT)"
}

View File

@@ -1,452 +1,91 @@
#!/usr/bin/env pwsh
<#!
.SYNOPSIS
Update agent context files with information from plan.md (PowerShell version)
.DESCRIPTION
Mirrors the behavior of scripts/bash/update-agent-context.sh:
1. Environment Validation
2. Plan Data Extraction
3. Agent File Management (create from template or update existing)
4. Content Generation (technology stack, recent changes, timestamp)
5. Multi-Agent Support (claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, roo, codebuddy, amp, shai, q, agy, bob, qodercli)
.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).
.EXAMPLE
./update-agent-context.ps1 -AgentType claude
.EXAMPLE
./update-agent-context.ps1 # Updates all existing agent files
.NOTES
Relies on common helper functions in common.ps1
#>
param(
[Parameter(Position=0)]
[ValidateSet('claude','gemini','copilot','cursor-agent','qwen','opencode','codex','windsurf','kilocode','auggie','roo','codebuddy','amp','shai','q','agy','bob','qodercli','generic')]
[string]$AgentType
)
[CmdletBinding()]
param([string]$AgentType)
$ErrorActionPreference = 'Stop'
# Import common helpers
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. (Join-Path $ScriptDir 'common.ps1')
$repoRoot = git rev-parse --show-toplevel
$currentBranch = git rev-parse --abbrev-ref HEAD
$featureDir = Join-Path $repoRoot "specs/$currentBranch"
$newPlan = Join-Path $featureDir 'plan.md'
if (-not (Test-Path $newPlan)) { Write-Error "ERROR: No plan.md found at $newPlan"; exit 1 }
# Acquire environment paths
$envData = Get-FeaturePathsEnv
$REPO_ROOT = $envData.REPO_ROOT
$CURRENT_BRANCH = $envData.CURRENT_BRANCH
$HAS_GIT = $envData.HAS_GIT
$IMPL_PLAN = $envData.IMPL_PLAN
$NEW_PLAN = $IMPL_PLAN
$claudeFile = Join-Path $repoRoot 'CLAUDE.md'
$geminiFile = Join-Path $repoRoot 'GEMINI.md'
$copilotFile = Join-Path $repoRoot '.github/copilot-instructions.md'
# Agent file paths
$CLAUDE_FILE = Join-Path $REPO_ROOT 'CLAUDE.md'
$GEMINI_FILE = Join-Path $REPO_ROOT 'GEMINI.md'
$COPILOT_FILE = Join-Path $REPO_ROOT '.github/agents/copilot-instructions.md'
$CURSOR_FILE = Join-Path $REPO_ROOT '.cursor/rules/specify-rules.mdc'
$QWEN_FILE = Join-Path $REPO_ROOT 'QWEN.md'
$AGENTS_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$WINDSURF_FILE = Join-Path $REPO_ROOT '.windsurf/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'
$ROO_FILE = Join-Path $REPO_ROOT '.roo/rules/specify-rules.md'
$CODEBUDDY_FILE = Join-Path $REPO_ROOT 'CODEBUDDY.md'
$QODER_FILE = Join-Path $REPO_ROOT 'QODER.md'
$AMP_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$SHAI_FILE = Join-Path $REPO_ROOT 'SHAI.md'
$Q_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
$AGY_FILE = Join-Path $REPO_ROOT '.agent/rules/specify-rules.md'
$BOB_FILE = Join-Path $REPO_ROOT 'AGENTS.md'
Write-Output "=== Updating agent context files for feature $currentBranch ==="
$TEMPLATE_FILE = Join-Path $REPO_ROOT '.specify/templates/agent-file-template.md'
# Parsed plan data placeholders
$script:NEW_LANG = ''
$script:NEW_FRAMEWORK = ''
$script:NEW_DB = ''
$script:NEW_PROJECT_TYPE = ''
function Write-Info {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Host "INFO: $Message"
function Get-PlanValue($pattern) {
if (-not (Test-Path $newPlan)) { return '' }
$line = Select-String -Path $newPlan -Pattern $pattern | Select-Object -First 1
if ($line) { return ($line.Line -replace "^\*\*$pattern\*\*: ", '') }
return ''
}
function Write-Success {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Host "$([char]0x2713) $Message"
$newLang = Get-PlanValue 'Language/Version'
$newFramework = Get-PlanValue 'Primary Dependencies'
$newTesting = Get-PlanValue 'Testing'
$newDb = Get-PlanValue 'Storage'
$newProjectType = Get-PlanValue 'Project Type'
function Initialize-AgentFile($targetFile, $agentName) {
if (Test-Path $targetFile) { return }
$template = Join-Path $repoRoot 'templates/agent-file-template.md'
if (-not (Test-Path $template)) { Write-Error "Template not found: $template"; return }
$content = Get-Content $template -Raw
$content = $content.Replace('[PROJECT NAME]', (Split-Path $repoRoot -Leaf))
$content = $content.Replace('[DATE]', (Get-Date -Format 'yyyy-MM-dd'))
$content = $content.Replace('[EXTRACTED FROM ALL PLAN.MD FILES]', "- $newLang + $newFramework ($currentBranch)")
if ($newProjectType -match 'web') { $structure = "backend/`nfrontend/`ntests/" } else { $structure = "src/`ntests/" }
$content = $content.Replace('[ACTUAL STRUCTURE FROM PLANS]', $structure)
if ($newLang -match 'Python') { $commands = 'cd src && pytest && ruff check .' }
elseif ($newLang -match 'Rust') { $commands = 'cargo test && cargo clippy' }
elseif ($newLang -match 'JavaScript|TypeScript') { $commands = 'npm test && npm run lint' }
else { $commands = "# Add commands for $newLang" }
$content = $content.Replace('[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]', $commands)
$content = $content.Replace('[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]', "${newLang}: Follow standard conventions")
$content = $content.Replace('[LAST 3 FEATURES AND WHAT THEY ADDED]', "- ${currentBranch}: Added ${newLang} + ${newFramework}")
$content | Set-Content $targetFile -Encoding UTF8
}
function Write-WarningMsg {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Warning $Message
}
function Write-Err {
param(
[Parameter(Mandatory=$true)]
[string]$Message
)
Write-Host "ERROR: $Message" -ForegroundColor Red
}
function Validate-Environment {
if (-not $CURRENT_BRANCH) {
Write-Err 'Unable to determine current feature'
if ($HAS_GIT) { Write-Info "Make sure you're on a feature branch" } else { Write-Info 'Set SPECIFY_FEATURE environment variable or create a feature first' }
exit 1
}
if (-not (Test-Path $NEW_PLAN)) {
Write-Err "No plan.md found at $NEW_PLAN"
Write-Info 'Ensure you are working on a feature with a corresponding spec directory'
if (-not $HAS_GIT) { Write-Info 'Use: $env:SPECIFY_FEATURE=your-feature-name or create a new feature first' }
exit 1
}
if (-not (Test-Path $TEMPLATE_FILE)) {
Write-Err "Template file not found at $TEMPLATE_FILE"
Write-Info 'Run specify init to scaffold .specify/templates, or add agent-file-template.md there.'
exit 1
function Update-AgentFile($targetFile, $agentName) {
if (-not (Test-Path $targetFile)) { Initialize-AgentFile $targetFile $agentName; return }
$content = Get-Content $targetFile -Raw
if ($newLang -and ($content -notmatch [regex]::Escape($newLang))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newLang + $newFramework ($currentBranch)`n" }
if ($newDb -and $newDb -ne 'N/A' -and ($content -notmatch [regex]::Escape($newDb))) { $content = $content -replace '(## Active Technologies\n)', "`$1- $newDb ($currentBranch)`n" }
if ($content -match '## Recent Changes\n([\s\S]*?)(\n\n|$)') {
$changesBlock = $matches[1].Trim().Split("`n")
$changesBlock = ,"- $currentBranch: Added $newLang + $newFramework" + $changesBlock
$changesBlock = $changesBlock | Where-Object { $_ } | Select-Object -First 3
$joined = ($changesBlock -join "`n")
$content = [regex]::Replace($content, '## Recent Changes\n([\s\S]*?)(\n\n|$)', "## Recent Changes`n$joined`n`n")
}
$content = [regex]::Replace($content, 'Last updated: \d{4}-\d{2}-\d{2}', "Last updated: $(Get-Date -Format 'yyyy-MM-dd')")
$content | Set-Content $targetFile -Encoding UTF8
Write-Output "$agentName context file updated successfully"
}
function Extract-PlanField {
param(
[Parameter(Mandatory=$true)]
[string]$FieldPattern,
[Parameter(Mandatory=$true)]
[string]$PlanFile
)
if (-not (Test-Path $PlanFile)) { return '' }
# Lines like **Language/Version**: Python 3.12
$regex = "^\*\*$([Regex]::Escape($FieldPattern))\*\*: (.+)$"
Get-Content -LiteralPath $PlanFile -Encoding utf8 | ForEach-Object {
if ($_ -match $regex) {
$val = $Matches[1].Trim()
if ($val -notin @('NEEDS CLARIFICATION','N/A')) { return $val }
switch ($AgentType) {
'claude' { Update-AgentFile $claudeFile 'Claude Code' }
'gemini' { Update-AgentFile $geminiFile 'Gemini CLI' }
'copilot' { Update-AgentFile $copilotFile 'GitHub Copilot' }
'' {
foreach ($pair in @(@{file=$claudeFile; name='Claude Code'}, @{file=$geminiFile; name='Gemini CLI'}, @{file=$copilotFile; name='GitHub Copilot'})) {
if (Test-Path $pair.file) { Update-AgentFile $pair.file $pair.name }
}
} | Select-Object -First 1
}
function Parse-PlanData {
param(
[Parameter(Mandatory=$true)]
[string]$PlanFile
)
if (-not (Test-Path $PlanFile)) { Write-Err "Plan file not found: $PlanFile"; return $false }
Write-Info "Parsing plan data from $PlanFile"
$script:NEW_LANG = Extract-PlanField -FieldPattern 'Language/Version' -PlanFile $PlanFile
$script:NEW_FRAMEWORK = Extract-PlanField -FieldPattern 'Primary Dependencies' -PlanFile $PlanFile
$script:NEW_DB = Extract-PlanField -FieldPattern 'Storage' -PlanFile $PlanFile
$script:NEW_PROJECT_TYPE = Extract-PlanField -FieldPattern 'Project Type' -PlanFile $PlanFile
if ($NEW_LANG) { Write-Info "Found language: $NEW_LANG" } else { Write-WarningMsg 'No language information found in plan' }
if ($NEW_FRAMEWORK) { Write-Info "Found framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Info "Found database: $NEW_DB" }
if ($NEW_PROJECT_TYPE) { Write-Info "Found project type: $NEW_PROJECT_TYPE" }
return $true
}
function Format-TechnologyStack {
param(
[Parameter(Mandatory=$false)]
[string]$Lang,
[Parameter(Mandatory=$false)]
[string]$Framework
)
$parts = @()
if ($Lang -and $Lang -ne 'NEEDS CLARIFICATION') { $parts += $Lang }
if ($Framework -and $Framework -notin @('NEEDS CLARIFICATION','N/A')) { $parts += $Framework }
if (-not $parts) { return '' }
return ($parts -join ' + ')
}
function Get-ProjectStructure {
param(
[Parameter(Mandatory=$false)]
[string]$ProjectType
)
if ($ProjectType -match 'web') { return "backend/`nfrontend/`ntests/" } else { return "src/`ntests/" }
}
function Get-CommandsForLanguage {
param(
[Parameter(Mandatory=$false)]
[string]$Lang
)
switch -Regex ($Lang) {
'Python' { return "cd src; pytest; ruff check ." }
'Rust' { return "cargo test; cargo clippy" }
'JavaScript|TypeScript' { return "npm test; npm run lint" }
default { return "# Add commands for $Lang" }
}
}
function Get-LanguageConventions {
param(
[Parameter(Mandatory=$false)]
[string]$Lang
)
if ($Lang) { "${Lang}: Follow standard conventions" } else { 'General: Follow standard conventions' }
}
function New-AgentFile {
param(
[Parameter(Mandatory=$true)]
[string]$TargetFile,
[Parameter(Mandatory=$true)]
[string]$ProjectName,
[Parameter(Mandatory=$true)]
[datetime]$Date
)
if (-not (Test-Path $TEMPLATE_FILE)) { Write-Err "Template not found at $TEMPLATE_FILE"; return $false }
$temp = New-TemporaryFile
Copy-Item -LiteralPath $TEMPLATE_FILE -Destination $temp -Force
$projectStructure = Get-ProjectStructure -ProjectType $NEW_PROJECT_TYPE
$commands = Get-CommandsForLanguage -Lang $NEW_LANG
$languageConventions = Get-LanguageConventions -Lang $NEW_LANG
$escaped_lang = $NEW_LANG
$escaped_framework = $NEW_FRAMEWORK
$escaped_branch = $CURRENT_BRANCH
$content = Get-Content -LiteralPath $temp -Raw -Encoding utf8
$content = $content -replace '\[PROJECT NAME\]',$ProjectName
$content = $content -replace '\[DATE\]',$Date.ToString('yyyy-MM-dd')
# Build the technology stack string safely
$techStackForTemplate = ""
if ($escaped_lang -and $escaped_framework) {
$techStackForTemplate = "- $escaped_lang + $escaped_framework ($escaped_branch)"
} elseif ($escaped_lang) {
$techStackForTemplate = "- $escaped_lang ($escaped_branch)"
} elseif ($escaped_framework) {
$techStackForTemplate = "- $escaped_framework ($escaped_branch)"
}
$content = $content -replace '\[EXTRACTED FROM ALL PLAN.MD FILES\]',$techStackForTemplate
# For project structure we manually embed (keep newlines)
$escapedStructure = [Regex]::Escape($projectStructure)
$content = $content -replace '\[ACTUAL STRUCTURE FROM PLANS\]',$escapedStructure
# Replace escaped newlines placeholder after all replacements
$content = $content -replace '\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]',$commands
$content = $content -replace '\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]',$languageConventions
# Build the recent changes string safely
$recentChangesForTemplate = ""
if ($escaped_lang -and $escaped_framework) {
$recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_lang} + ${escaped_framework}"
} elseif ($escaped_lang) {
$recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_lang}"
} elseif ($escaped_framework) {
$recentChangesForTemplate = "- ${escaped_branch}: Added ${escaped_framework}"
}
$content = $content -replace '\[LAST 3 FEATURES AND WHAT THEY ADDED\]',$recentChangesForTemplate
# Convert literal \n sequences introduced by Escape to real newlines
$content = $content -replace '\\n',[Environment]::NewLine
$parent = Split-Path -Parent $TargetFile
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
Remove-Item $temp -Force
return $true
}
function Update-ExistingAgentFile {
param(
[Parameter(Mandatory=$true)]
[string]$TargetFile,
[Parameter(Mandatory=$true)]
[datetime]$Date
)
if (-not (Test-Path $TargetFile)) { return (New-AgentFile -TargetFile $TargetFile -ProjectName (Split-Path $REPO_ROOT -Leaf) -Date $Date) }
$techStack = Format-TechnologyStack -Lang $NEW_LANG -Framework $NEW_FRAMEWORK
$newTechEntries = @()
if ($techStack) {
$escapedTechStack = [Regex]::Escape($techStack)
if (-not (Select-String -Pattern $escapedTechStack -Path $TargetFile -Quiet)) {
$newTechEntries += "- $techStack ($CURRENT_BRANCH)"
if (-not (Test-Path $claudeFile) -and -not (Test-Path $geminiFile) -and -not (Test-Path $copilotFile)) {
Write-Output 'No agent context files found. Creating Claude Code context file by default.'
Update-AgentFile $claudeFile 'Claude Code'
}
}
if ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) {
$escapedDB = [Regex]::Escape($NEW_DB)
if (-not (Select-String -Pattern $escapedDB -Path $TargetFile -Quiet)) {
$newTechEntries += "- $NEW_DB ($CURRENT_BRANCH)"
}
}
$newChangeEntry = ''
if ($techStack) { $newChangeEntry = "- ${CURRENT_BRANCH}: Added ${techStack}" }
elseif ($NEW_DB -and $NEW_DB -notin @('N/A','NEEDS CLARIFICATION')) { $newChangeEntry = "- ${CURRENT_BRANCH}: Added ${NEW_DB}" }
$lines = Get-Content -LiteralPath $TargetFile -Encoding utf8
$output = New-Object System.Collections.Generic.List[string]
$inTech = $false; $inChanges = $false; $techAdded = $false; $changeAdded = $false; $existingChanges = 0
for ($i=0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
if ($line -eq '## Active Technologies') {
$output.Add($line)
$inTech = $true
continue
}
if ($inTech -and $line -match '^##\s') {
if (-not $techAdded -and $newTechEntries.Count -gt 0) { $newTechEntries | ForEach-Object { $output.Add($_) }; $techAdded = $true }
$output.Add($line); $inTech = $false; continue
}
if ($inTech -and [string]::IsNullOrWhiteSpace($line)) {
if (-not $techAdded -and $newTechEntries.Count -gt 0) { $newTechEntries | ForEach-Object { $output.Add($_) }; $techAdded = $true }
$output.Add($line); continue
}
if ($line -eq '## Recent Changes') {
$output.Add($line)
if ($newChangeEntry) { $output.Add($newChangeEntry); $changeAdded = $true }
$inChanges = $true
continue
}
if ($inChanges -and $line -match '^##\s') { $output.Add($line); $inChanges = $false; continue }
if ($inChanges -and $line -match '^- ') {
if ($existingChanges -lt 2) { $output.Add($line); $existingChanges++ }
continue
}
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')))
continue
}
$output.Add($line)
}
# Post-loop check: if we're still in the Active Technologies section and haven't added new entries
if ($inTech -and -not $techAdded -and $newTechEntries.Count -gt 0) {
$newTechEntries | ForEach-Object { $output.Add($_) }
}
Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8
return $true
Default { Write-Error "ERROR: Unknown agent type '$AgentType'. Use: claude, gemini, copilot, or leave empty for all."; exit 1 }
}
function Update-AgentFile {
param(
[Parameter(Mandatory=$true)]
[string]$TargetFile,
[Parameter(Mandatory=$true)]
[string]$AgentName
)
if (-not $TargetFile -or -not $AgentName) { Write-Err 'Update-AgentFile requires TargetFile and AgentName'; return $false }
Write-Info "Updating $AgentName context file: $TargetFile"
$projectName = Split-Path $REPO_ROOT -Leaf
$date = Get-Date
$dir = Split-Path -Parent $TargetFile
if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir | Out-Null }
if (-not (Test-Path $TargetFile)) {
if (New-AgentFile -TargetFile $TargetFile -ProjectName $projectName -Date $date) { Write-Success "Created new $AgentName context file" } else { Write-Err 'Failed to create new agent file'; return $false }
} else {
try {
if (Update-ExistingAgentFile -TargetFile $TargetFile -Date $date) { Write-Success "Updated existing $AgentName context file" } else { Write-Err 'Failed to update agent file'; return $false }
} catch {
Write-Err "Cannot access or update existing file: $TargetFile. $_"
return $false
}
}
return $true
}
function Update-SpecificAgent {
param(
[Parameter(Mandatory=$true)]
[string]$Type
)
switch ($Type) {
'claude' { Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code' }
'gemini' { Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI' }
'copilot' { Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot' }
'cursor-agent' { Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE' }
'qwen' { Update-AgentFile -TargetFile $QWEN_FILE -AgentName 'Qwen Code' }
'opencode' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'opencode' }
'codex' { Update-AgentFile -TargetFile $AGENTS_FILE -AgentName 'Codex CLI' }
'windsurf' { Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf' }
'kilocode' { Update-AgentFile -TargetFile $KILOCODE_FILE -AgentName 'Kilo Code' }
'auggie' { Update-AgentFile -TargetFile $AUGGIE_FILE -AgentName 'Auggie CLI' }
'roo' { Update-AgentFile -TargetFile $ROO_FILE -AgentName 'Roo Code' }
'codebuddy' { Update-AgentFile -TargetFile $CODEBUDDY_FILE -AgentName 'CodeBuddy CLI' }
'qodercli' { Update-AgentFile -TargetFile $QODER_FILE -AgentName 'Qoder CLI' }
'amp' { Update-AgentFile -TargetFile $AMP_FILE -AgentName 'Amp' }
'shai' { Update-AgentFile -TargetFile $SHAI_FILE -AgentName 'SHAI' }
'q' { Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer CLI' }
'agy' { Update-AgentFile -TargetFile $AGY_FILE -AgentName 'Antigravity' }
'bob' { Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob' }
'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|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic'; return $false }
}
}
function Update-AllExistingAgents {
$found = $false
$ok = $true
if (Test-Path $CLAUDE_FILE) { if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }; $found = $true }
if (Test-Path $GEMINI_FILE) { if (-not (Update-AgentFile -TargetFile $GEMINI_FILE -AgentName 'Gemini CLI')) { $ok = $false }; $found = $true }
if (Test-Path $COPILOT_FILE) { if (-not (Update-AgentFile -TargetFile $COPILOT_FILE -AgentName 'GitHub Copilot')) { $ok = $false }; $found = $true }
if (Test-Path $CURSOR_FILE) { if (-not (Update-AgentFile -TargetFile $CURSOR_FILE -AgentName 'Cursor IDE')) { $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 $WINDSURF_FILE) { if (-not (Update-AgentFile -TargetFile $WINDSURF_FILE -AgentName 'Windsurf')) { $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 $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 $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 $Q_FILE) { if (-not (Update-AgentFile -TargetFile $Q_FILE -AgentName 'Amazon Q Developer 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 $BOB_FILE) { if (-not (Update-AgentFile -TargetFile $BOB_FILE -AgentName 'IBM Bob')) { $ok = $false }; $found = $true }
if (-not $found) {
Write-Info 'No existing agent files found, creating default Claude file...'
if (-not (Update-AgentFile -TargetFile $CLAUDE_FILE -AgentName 'Claude Code')) { $ok = $false }
}
return $ok
}
function Print-Summary {
Write-Host ''
Write-Info 'Summary of changes:'
if ($NEW_LANG) { Write-Host " - Added language: $NEW_LANG" }
if ($NEW_FRAMEWORK) { Write-Host " - Added framework: $NEW_FRAMEWORK" }
if ($NEW_DB -and $NEW_DB -ne 'N/A') { Write-Host " - Added database: $NEW_DB" }
Write-Host ''
Write-Info 'Usage: ./update-agent-context.ps1 [-AgentType claude|gemini|copilot|cursor-agent|qwen|opencode|codex|windsurf|kilocode|auggie|roo|codebuddy|amp|shai|q|agy|bob|qodercli|generic]'
}
function Main {
Validate-Environment
Write-Info "=== Updating agent context files for feature $CURRENT_BRANCH ==="
if (-not (Parse-PlanData -PlanFile $NEW_PLAN)) { Write-Err 'Failed to parse plan data'; exit 1 }
$success = $true
if ($AgentType) {
Write-Info "Updating specific agent: $AgentType"
if (-not (Update-SpecificAgent -Type $AgentType)) { $success = $false }
}
else {
Write-Info 'No agent specified, updating all existing agent files...'
if (-not (Update-AllExistingAgents)) { $success = $false }
}
Print-Summary
if ($success) { Write-Success 'Agent context update completed successfully'; exit 0 } else { Write-Err 'Agent context update completed with errors'; exit 1 }
}
Main
Write-Output ''
Write-Output 'Summary of changes:'
if ($newLang) { Write-Output "- Added language: $newLang" }
if ($newFramework) { Write-Output "- Added framework: $newFramework" }
if ($newDb -and $newDb -ne 'N/A') { Write-Output "- Added database: $newDb" }
Write-Output ''
Write-Output 'Usage: ./update-agent-context.ps1 [claude|gemini|copilot]'

View File

@@ -2,15 +2,15 @@
## The Power Inversion
For decades, code has been king. Specifications served code—they were the scaffolding we built and then discarded once the "real work" of coding began. We wrote PRDs to guide development, created design docs to inform implementation, drew diagrams to visualize architecture. But these were always subordinate to the code itself. Code was truth. Everything else was, at best, good intentions. Code was the source of truth, and as it moved forward, specs rarely kept pace. As the asset (code) and the implementation are one, it's not easy to have a parallel implementation without trying to build from the code.
For decades, code has been king. Specifications served code—they were the scaffolding we built and then discarded once the "real work" of coding began. We wrote PRDs to guide development, created design docs to inform implementation, drew diagrams to visualize architecture. But these were always subordinate to the code itself. Code was truth. Everything else was, at best, good intentions. Code was the source of truth, as it moved forward, and spec's rarely kept pace. As the asset (code) and the implementation are one, it's not easy to have a parallel implementation without trying to build from the code.
Spec-Driven Development (SDD) inverts this power structure. Specifications don't serve code—code serves specifications. The Product Requirements Document (PRD) isn't a guide for implementation; it's the source that generates implementation. Technical plans aren't documents that inform coding; they're precise definitions that produce code. This isn't an incremental improvement to how we build software. It's a fundamental rethinking of what drives development.
Spec-Driven Development (SDD) inverts this power structure. Specifications don't serve code—code serves specifications. The (Product Requirements Document-Specification) PRD isn't a guide for implementation; it's the source that generates implementation. Technical plans aren't documents that inform coding; they're precise definitions that produce code. This isn't an incremental improvement to how we build software. It's a fundamental rethinking of what drives development.
The gap between specification and implementation has plagued software development since its inception. We've tried to bridge it with better documentation, more detailed requirements, stricter processes. These approaches fail because they accept the gap as inevitable. They try to narrow it but never eliminate it. SDD eliminates the gap by making specifications and their concrete implementation plans born from the specification executable. When specifications and implementation plans generate code, there is no gap—only transformation.
The gap between specification and implementation has plagued software development since its inception. We've tried to bridge it with better documentation, more detailed requirements, stricter processes. These approaches fail because they accept the gap as inevitable. They try to narrow it but never eliminate it. SDD eliminates the gap by making specifications or and their concrete implementation plans born from the specification executable. When specifications to implementation plans generate code, there is no gap—only transformation.
This transformation is now possible because AI can understand and implement complex specifications, and create detailed implementation plans. But raw AI generation without structure produces chaos. SDD provides that structure through specifications and subsequent implementation plans that are precise, complete, and unambiguous enough to generate working systems. The specification becomes the primary artifact. Code becomes its expression (as an implementation from the implementation plan) in a particular language and framework.
In this new world, maintaining software means evolving specifications. The intent of the development team is expressed in natural language ("**intent-driven development**"), design assets, core principles and other guidelines. The **lingua franca** of development moves to a higher level, and code is the last-mile approach.
In this new world, maintaining software means evolving specifications. The intent of the development team is expressed in natural language ("**intent-driven development**"), design assets, core principles and other guidelines . The **lingua franca** of development moves to a higher-level, and code is the last-mile approach.
Debugging means fixing specifications and their implementation plans that generate incorrect code. Refactoring means restructuring for clarity. The entire development workflow reorganizes around specifications as the central source of truth, with implementation plans and code as the continuously regenerated output. Updating apps with new features or creating a new parallel implementation because we are creative beings, means revisiting the specification and creating new implementation plans. This process is therefore a 0 -> 1, (1', ..), 2, 3, N.
@@ -18,7 +18,7 @@ The development team focuses in on their creativity, experimentation, their crit
## The SDD Workflow in Practice
The workflow begins with an idea—often vague and incomplete. Through iterative dialogue with AI, this idea becomes a comprehensive PRD. The AI asks clarifying questions, identifies edge cases, and helps define precise acceptance criteria. What might take days of meetings and documentation in traditional development happens in hours of focused specification work. This transforms the traditional SDLC—requirements and design become continuous activities rather than discrete phases. This is supportive of a **team process**, where team-reviewed specifications are expressed and versioned, created in branches, and merged.
The workflow begins with an idea—often vague and incomplete. Through iterative dialogue with AI, this idea becomes a comprehensive PRD. The AI asks clarifying questions, identifies edge cases, and helps define precise acceptance criteria. What might take days of meetings and documentation in traditional development happens in hours of focused specification work. This transforms the traditional SDLC—requirements and design become continuous activities rather than discrete phases. This is supportive of a **team process**, that's team reviewed-specifications are expressed and versioned, created in branches, and merged.
When a product manager updates acceptance criteria, implementation plans automatically flag affected technical decisions. When an architect discovers a better pattern, the PRD updates to reflect new possibilities.
@@ -34,13 +34,13 @@ The feedback loop extends beyond initial development. Production metrics and inc
Three trends make SDD not just possible but necessary:
First, AI capabilities have reached a threshold where natural language specifications can reliably generate working code. This isn't about replacing developers—it's about amplifying their effectiveness by automating the mechanical translation from specification to implementation. It can amplify exploration and creativity, support "start-over" easily, and support addition, subtraction, and critical thinking.
First, AI capabilities have reached a threshold where natural language specifications can reliably generate working code. This isn't about replacing developers—it's about amplifying their effectiveness by automating the mechanical translation from specification to implementation. It can amplify exploration and creativity, it can support "start-over" easily, it supports addition subtraction and critical thinking.
Second, software complexity continues to grow exponentially. Modern systems integrate dozens of services, frameworks, and dependencies. Keeping all these pieces aligned with original intent through manual processes becomes increasingly difficult. SDD provides systematic alignment through specification-driven generation. Frameworks may evolve to provide AI-first support, not human-first support, or architect around reusable components.
Third, the pace of change accelerates. Requirements change far more rapidly today than ever before. Pivoting is no longer exceptional—it's expected. Modern product development demands rapid iteration based on user feedback, market conditions, and competitive pressures. Traditional development treats these changes as disruptions. Each pivot requires manually propagating changes through documentation, design, and code. The result is either slow, careful updates that limit velocity, or fast, reckless changes that accumulate technical debt.
SDD can support what-if/simulation experiments: "If we need to re-implement or change the application to promote a business need to sell more T-shirts, how would we implement and experiment for that?"
SDD can support what-if/simulation experiments, "If we need to re-implement or change the application to promote a business need to sell more T-shirts, how would we implement and experiment for that?".
SDD transforms requirement changes from obstacles into normal workflow. When specifications drive implementation, pivots become systematic regenerations rather than manual rewrites. Change a core requirement in the PRD, and affected implementation plans update automatically. Modify a user story, and corresponding API endpoints regenerate. This isn't just about initial development—it's about maintaining engineering velocity through inevitable changes.
@@ -74,7 +74,7 @@ The key is treating specifications as the source of truth, with code as the gene
The SDD methodology is significantly enhanced through three powerful commands that automate the specification → planning → tasking workflow:
### The `/speckit.specify` Command
### The `/specify` Command
This command transforms a simple feature description (the user-prompt) into a complete, structured specification with automatic repository management:
@@ -83,7 +83,7 @@ This command transforms a simple feature description (the user-prompt) into a co
3. **Template-Based Generation**: Copies and customizes the feature specification template with your requirements
4. **Directory Structure**: Creates the proper `specs/[branch-name]/` structure for all related documents
### The `/speckit.plan` Command
### The `/plan` Command
Once a feature specification exists, this command creates a comprehensive implementation plan:
@@ -93,7 +93,7 @@ Once a feature specification exists, this command creates a comprehensive implem
4. **Detailed Documentation**: Generates supporting documents for data models, API contracts, and test scenarios
5. **Quickstart Validation**: Produces a quickstart guide capturing key validation scenarios
### The `/speckit.tasks` Command
### The `/tasks` Command
After a plan is created, this command analyzes the plan and related design documents to generate an executable task list:
@@ -121,7 +121,7 @@ Total: ~12 hours of documentation work
```bash
# Step 1: Create the feature specification (5 minutes)
/speckit.specify Real-time chat system with message history and user presence
/specify Real-time chat system with message history and user presence
# This automatically:
# - Creates branch "003-chat-system"
@@ -129,10 +129,10 @@ Total: ~12 hours of documentation work
# - Populates it with structured requirements
# Step 2: Generate implementation plan (5 minutes)
/speckit.plan WebSocket for real-time messaging, PostgreSQL for history, Redis for presence
/plan WebSocket for real-time messaging, PostgreSQL for history, Redis for presence
# Step 3: Generate executable tasks (5 minutes)
/speckit.tasks
/tasks
# This automatically creates:
# - specs/003-chat-system/plan.md
@@ -195,7 +195,6 @@ The templates include comprehensive checklists that act as "unit tests" for the
```markdown
### Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
@@ -209,14 +208,10 @@ The implementation plan template enforces architectural principles through phase
```markdown
### Phase -1: Pre-Implementation Gates
#### Simplicity Gate (Article VII)
- [ ] Using ≤3 projects?
- [ ] No future-proofing?
#### Anti-Abstraction Gate (Article VIII)
- [ ] Using framework directly?
- [ ] Single model representation?
```
@@ -352,19 +347,15 @@ The implementation plan template operationalizes these articles through concrete
```markdown
### Phase -1: Pre-Implementation Gates
#### Simplicity Gate (Article VII)
- [ ] Using ≤3 projects?
- [ ] No future-proofing?
#### Anti-Abstraction Gate (Article VIII)
- [ ] Using framework directly?
- [ ] Single model representation?
#### Integration-First Gate (Article IX)
- [ ] Contracts defined?
- [ ] Contract tests written?
```

View File

@@ -1,8 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {}
}

5
src/apm_cli/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""APM-CLI package."""
from .version import get_version
__version__ = get_version()

View File

@@ -0,0 +1 @@
"""Adapters package."""

View File

@@ -0,0 +1 @@
"""Client adapters package."""

View File

@@ -0,0 +1,39 @@
"""Base adapter interface for MCP clients."""
from abc import ABC, abstractmethod
class MCPClientAdapter(ABC):
"""Base adapter for MCP clients."""
@abstractmethod
def get_config_path(self):
"""Get the path to the MCP configuration file."""
pass
@abstractmethod
def update_config(self, config_updates):
"""Update the MCP configuration."""
pass
@abstractmethod
def get_current_config(self):
"""Get the current MCP configuration."""
pass
@abstractmethod
def configure_mcp_server(self, server_url, server_name=None, enabled=True, env_overrides=None, server_info_cache=None, runtime_vars=None):
"""Configure an MCP server in the client configuration.
Args:
server_url (str): URL of the MCP server.
server_name (str, optional): Name of the server. Defaults to None.
enabled (bool, optional): Whether to enable the server. Defaults to True.
env_overrides (dict, optional): Environment variable overrides. Defaults to None.
server_info_cache (dict, optional): Pre-fetched server info to avoid duplicate registry calls.
runtime_vars (dict, optional): Runtime variable values. Defaults to None.
Returns:
bool: True if successful, False otherwise.
"""
pass

View File

@@ -0,0 +1,528 @@
"""OpenAI Codex CLI implementation of MCP client adapter.
This adapter implements the Codex CLI-specific handling of MCP server configuration,
targeting the global ~/.codex/config.toml file as specified in the MCP installation
architecture specification.
"""
import os
import toml
from pathlib import Path
from .base import MCPClientAdapter
from ...registry.client import SimpleRegistryClient
from ...registry.integration import RegistryIntegration
class CodexClientAdapter(MCPClientAdapter):
"""Codex CLI implementation of MCP client adapter.
This adapter handles Codex CLI-specific configuration for MCP servers using
a global ~/.codex/config.toml file, following the TOML format for
MCP server configuration.
"""
def __init__(self, registry_url=None):
"""Initialize the Codex CLI client adapter.
Args:
registry_url (str, optional): URL of the MCP registry.
If not provided, uses the MCP_REGISTRY_URL environment variable
or falls back to the default GitHub registry.
"""
self.registry_client = SimpleRegistryClient(registry_url)
self.registry_integration = RegistryIntegration(registry_url)
def get_config_path(self):
"""Get the path to the Codex CLI MCP configuration file.
Returns:
str: Path to ~/.codex/config.toml
"""
codex_dir = Path.home() / ".codex"
return str(codex_dir / "config.toml")
def update_config(self, config_updates):
"""Update the Codex CLI MCP configuration.
Args:
config_updates (dict): Configuration updates to apply.
"""
current_config = self.get_current_config()
# Ensure mcp_servers section exists
if "mcp_servers" not in current_config:
current_config["mcp_servers"] = {}
# Apply updates to mcp_servers section
current_config["mcp_servers"].update(config_updates)
# Write back to file
config_path = Path(self.get_config_path())
# Ensure directory exists
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
toml.dump(current_config, f)
def get_current_config(self):
"""Get the current Codex CLI MCP configuration.
Returns:
dict: Current configuration, or empty dict if file doesn't exist.
"""
config_path = self.get_config_path()
if not os.path.exists(config_path):
return {}
try:
with open(config_path, 'r') as f:
return toml.load(f)
except (toml.TomlDecodeError, IOError):
return {}
def configure_mcp_server(self, server_url, server_name=None, enabled=True, env_overrides=None, server_info_cache=None, runtime_vars=None):
"""Configure an MCP server in Codex CLI configuration.
This method follows the Codex CLI MCP configuration format with
mcp_servers sections in the TOML configuration.
Args:
server_url (str): URL or identifier of the MCP server.
server_name (str, optional): Name of the server. Defaults to None.
enabled (bool, optional): Ignored parameter, kept for API compatibility.
env_overrides (dict, optional): Pre-collected environment variable overrides.
server_info_cache (dict, optional): Pre-fetched server info to avoid duplicate registry calls.
runtime_vars (dict, optional): Runtime variable values. Defaults to None.
Returns:
bool: True if successful, False otherwise.
"""
if not server_url:
print("Error: server_url cannot be empty")
return False
try:
# Use cached server info if available, otherwise fetch from registry
if server_info_cache and server_url in server_info_cache:
server_info = server_info_cache[server_url]
else:
# Fallback to registry lookup if not cached
server_info = self.registry_client.find_server_by_reference(server_url)
# Fail if server is not found in registry - security requirement
if not server_info:
print(f"Error: MCP server '{server_url}' not found in registry")
return False
# Check for remote servers early - Codex doesn't support remote/SSE servers
remotes = server_info.get("remotes", [])
packages = server_info.get("packages", [])
# If server has only remote endpoints and no packages, it's a remote-only server
if remotes and not packages:
print(f"⚠️ Warning: MCP server '{server_url}' is a remote server (SSE type)")
print(" Codex CLI only supports local servers with command/args configuration")
print(" Remote servers are not supported by Codex CLI")
print(" Skipping installation for Codex CLI")
return False
# Determine the server name for configuration key
if server_name:
# Use explicitly provided server name
config_key = server_name
else:
# Extract name from server_url (part after last slash)
# For URLs like "microsoft/azure-devops-mcp" -> "azure-devops-mcp"
# For URLs like "github/github-mcp-server" -> "github-mcp-server"
if '/' in server_url:
config_key = server_url.split('/')[-1]
else:
# Fallback to full server_url if no slash
config_key = server_url
# Generate server configuration with environment variable resolution
server_config = self._format_server_config(server_info, env_overrides, runtime_vars)
# Update configuration using the chosen key
self.update_config({config_key: server_config})
print(f"Successfully configured MCP server '{config_key}' for Codex CLI")
return True
except Exception as e:
print(f"Error configuring MCP server: {e}")
return False
def _format_server_config(self, server_info, env_overrides=None, runtime_vars=None):
"""Format server information into Codex CLI MCP configuration format.
Args:
server_info (dict): Server information from registry.
env_overrides (dict, optional): Pre-collected environment variable overrides.
runtime_vars (dict, optional): Runtime variable values.
Returns:
dict: Formatted server configuration for Codex CLI.
"""
# Default configuration structure with registry ID for conflict detection
config = {
"command": "unknown",
"args": [],
"env": {},
"id": server_info.get("id", "") # Add registry UUID for conflict detection
}
# Note: Remote servers (SSE type) are handled in configure_mcp_server and rejected early
# This method only handles local servers with packages
# Get packages from server info
packages = server_info.get("packages", [])
if not packages:
# If no packages are available, this indicates incomplete server configuration
# This should fail installation with a clear error message
raise ValueError(f"MCP server has no package information available in registry. "
f"This appears to be a temporary registry issue or the server is remote-only. "
f"Server: {server_info.get('name', 'unknown')}")
if packages:
# Use the first package for configuration (prioritize npm, then docker, then others)
package = self._select_best_package(packages)
if package:
registry_name = package.get("registry_name", "")
package_name = package.get("name", "")
runtime_hint = package.get("runtime_hint", "")
runtime_arguments = package.get("runtime_arguments", [])
package_arguments = package.get("package_arguments", [])
env_vars = package.get("environment_variables", [])
# Resolve environment variables first
resolved_env = self._process_environment_variables(env_vars, env_overrides)
# Process arguments to extract simple string values
processed_runtime_args = self._process_arguments(runtime_arguments, resolved_env, runtime_vars)
processed_package_args = self._process_arguments(package_arguments, resolved_env, runtime_vars)
# Generate command and args based on package type
if registry_name == "npm":
config["command"] = runtime_hint or "npx"
# For npm packages, use runtime_arguments directly as they contain the complete npx command
config["args"] = processed_runtime_args + processed_package_args
# For NPM packages, also use env block for environment variables
if resolved_env:
config["env"] = resolved_env
elif registry_name == "docker":
config["command"] = "docker"
# For Docker packages in Codex TOML format:
# - Ensure all environment variables from resolved_env are represented as -e flags in args
# - Put actual environment variable values in separate [env] section
config["args"] = self._ensure_docker_env_flags(processed_runtime_args + processed_package_args, resolved_env)
# Environment variables go in separate env section for Codex TOML format
if resolved_env:
config["env"] = resolved_env
elif registry_name == "pypi":
config["command"] = runtime_hint or "uvx"
config["args"] = [package_name] + processed_runtime_args + processed_package_args
# For PyPI packages, use env block for environment variables
if resolved_env:
config["env"] = resolved_env
elif registry_name == "homebrew":
# For homebrew packages, assume the binary name is the command
config["command"] = package_name.split('/')[-1] if '/' in package_name else package_name
config["args"] = processed_runtime_args + processed_package_args
# For Homebrew packages, use env block for environment variables
if resolved_env:
config["env"] = resolved_env
else:
# Generic package handling
config["command"] = runtime_hint or package_name
config["args"] = processed_runtime_args + processed_package_args
# For generic packages, use env block for environment variables
if resolved_env:
config["env"] = resolved_env
return config
def _process_arguments(self, arguments, resolved_env=None, runtime_vars=None):
"""Process argument objects to extract simple string values with environment resolution.
Args:
arguments (list): List of argument objects from registry.
resolved_env (dict): Resolved environment variables.
runtime_vars (dict): Runtime variable values.
Returns:
list: List of processed argument strings.
"""
if resolved_env is None:
resolved_env = {}
if runtime_vars is None:
runtime_vars = {}
processed = []
for arg in arguments:
if isinstance(arg, dict):
# Extract value from argument object
arg_type = arg.get("type", "")
if arg_type == "positional":
value = arg.get("value", arg.get("default", ""))
if value:
# Resolve both environment and runtime variable placeholders with actual values
processed_value = self._resolve_variable_placeholders(str(value), resolved_env, runtime_vars)
processed.append(processed_value)
elif arg_type == "named":
# For named arguments, the flag name is in the "value" field
flag_name = arg.get("value", "")
if flag_name:
processed.append(flag_name)
# Some named arguments might have additional values (rare)
additional_value = arg.get("name", "")
if additional_value and additional_value != flag_name and not additional_value.startswith("-"):
processed_value = self._resolve_variable_placeholders(str(additional_value), resolved_env, runtime_vars)
processed.append(processed_value)
elif isinstance(arg, str):
# Already a string, use as-is but resolve variable placeholders
processed_value = self._resolve_variable_placeholders(arg, resolved_env, runtime_vars)
processed.append(processed_value)
return processed
def _process_environment_variables(self, env_vars, env_overrides=None):
"""Process environment variable definitions and resolve actual values.
Args:
env_vars (list): List of environment variable definitions.
env_overrides (dict, optional): Pre-collected environment variable overrides.
Returns:
dict: Dictionary of resolved environment variable values.
"""
import os
import sys
from rich.prompt import Prompt
resolved = {}
env_overrides = env_overrides or {}
# If env_overrides is provided, it means the CLI has already handled environment variable collection
# In this case, we should NEVER prompt for additional variables
skip_prompting = bool(env_overrides)
# Check for CI/automated environment via APM_E2E_TESTS flag (more reliable than TTY detection)
if os.getenv('APM_E2E_TESTS') == '1':
skip_prompting = True
print(f"💡 APM_E2E_TESTS detected, will skip environment variable prompts")
# Also skip prompting if we're in a non-interactive environment (fallback)
is_interactive = sys.stdin.isatty() and sys.stdout.isatty()
if not is_interactive:
skip_prompting = True
# Add default GitHub MCP server environment variables for essential functionality first
# This ensures variables have defaults when user provides empty values or they're optional
default_github_env = {
"GITHUB_TOOLSETS": "context",
"GITHUB_DYNAMIC_TOOLSETS": "1"
}
# Track which variables were explicitly provided with empty values (user wants defaults)
empty_value_vars = set()
if env_overrides:
for key, value in env_overrides.items():
if key in env_overrides and (not value or not value.strip()):
empty_value_vars.add(key)
for env_var in env_vars:
if isinstance(env_var, dict):
name = env_var.get("name", "")
description = env_var.get("description", "")
required = env_var.get("required", True)
if name:
# First check overrides, then environment
value = env_overrides.get(name) or os.getenv(name)
# Only prompt if not provided in overrides or environment AND it's required AND we're not in managed override mode
if not value and required and not skip_prompting:
# Only prompt if not provided in overrides
prompt_text = f"Enter value for {name}"
if description:
prompt_text += f" ({description})"
value = Prompt.ask(prompt_text, password=True if "token" in name.lower() or "key" in name.lower() else False)
# Add variable if it has a value OR if user explicitly provided empty and we have a default
if value and value.strip():
resolved[name] = value
elif name in empty_value_vars and name in default_github_env:
# User provided empty value and we have a default - use default
resolved[name] = default_github_env[name]
elif not required and name in default_github_env:
# Variable is optional and we have a default - use default
resolved[name] = default_github_env[name]
elif skip_prompting and name in default_github_env:
# Non-interactive environment and we have a default - use default
resolved[name] = default_github_env[name]
return resolved
def _resolve_variable_placeholders(self, value, resolved_env, runtime_vars):
"""Resolve both environment and runtime variable placeholders in values.
Args:
value (str): Value that may contain placeholders like <TOKEN_NAME> or {runtime_var}
resolved_env (dict): Dictionary of resolved environment variables.
runtime_vars (dict): Dictionary of resolved runtime variables.
Returns:
str: Processed value with actual variable values.
"""
import re
if not value:
return value
processed = str(value)
# Replace <TOKEN_NAME> with actual values from resolved_env (for Docker env vars)
env_pattern = r'<([A-Z_][A-Z0-9_]*)>'
def replace_env_var(match):
env_name = match.group(1)
return resolved_env.get(env_name, match.group(0)) # Return original if not found
processed = re.sub(env_pattern, replace_env_var, processed)
# Replace {runtime_var} with actual values from runtime_vars
runtime_pattern = r'\{([a-zA-Z_][a-zA-Z0-9_]*)\}'
def replace_runtime_var(match):
var_name = match.group(1)
return runtime_vars.get(var_name, match.group(0)) # Return original if not found
processed = re.sub(runtime_pattern, replace_runtime_var, processed)
return processed
def _resolve_env_placeholders(self, value, resolved_env):
"""Legacy method for backward compatibility. Use _resolve_variable_placeholders instead."""
return self._resolve_variable_placeholders(value, resolved_env, {})
def _ensure_docker_env_flags(self, base_args, env_vars):
"""Ensure all environment variables are represented as -e flags in Docker args.
For Codex TOML format, Docker args should contain -e flags for ALL environment variables
that will be available to the container, while actual values go in the [env] section.
Args:
base_args (list): Base Docker arguments from registry.
env_vars (dict): All environment variables that should be available.
Returns:
list: Docker arguments with -e flags for all environment variables.
"""
if not env_vars:
return base_args
result = []
existing_env_vars = set()
# First pass: collect existing -e flags and build result with existing args
i = 0
while i < len(base_args):
arg = base_args[i]
result.append(arg)
# Track existing -e flags
if arg == "-e" and i + 1 < len(base_args):
env_var_name = base_args[i + 1]
existing_env_vars.add(env_var_name)
result.append(env_var_name)
i += 2
else:
i += 1
# Second pass: add -e flags for any environment variables not already present
# Insert them after "run" but before the image name (last argument)
image_name = result[-1] if result else ""
if image_name and not image_name.startswith("-"):
# Remove image name temporarily
result.pop()
# Add missing environment variable flags
for env_name in sorted(env_vars.keys()):
if env_name not in existing_env_vars:
result.extend(["-e", env_name])
# Add image name back
result.append(image_name)
else:
# If we can't identify image name, just append at the end
for env_name in sorted(env_vars.keys()):
if env_name not in existing_env_vars:
result.extend(["-e", env_name])
return result
def _inject_docker_env_vars(self, args, env_vars):
"""Inject environment variables into Docker arguments as -e flags.
Args:
args (list): Original Docker arguments.
env_vars (dict): Environment variables to inject.
Returns:
list: Updated arguments with environment variables injected as -e flags.
"""
if not env_vars:
return args
result = []
existing_env_vars = set()
# First pass: collect existing -e flags to avoid duplicates
i = 0
while i < len(args):
if args[i] == "-e" and i + 1 < len(args):
existing_env_vars.add(args[i + 1])
i += 2
else:
i += 1
# Second pass: build the result with new env vars injected after "run"
for i, arg in enumerate(args):
result.append(arg)
# If this is a docker run command, inject new environment variables after "run"
if arg == "run":
for env_name in env_vars.keys():
if env_name not in existing_env_vars:
result.extend(["-e", env_name])
return result
def _select_best_package(self, packages):
"""Select the best package for installation from available packages.
Prioritizes packages in order: npm, docker, pypi, homebrew, others.
Args:
packages (list): List of package dictionaries.
Returns:
dict: Best package to use, or None if no suitable package found.
"""
priority_order = ["npm", "docker", "pypi", "homebrew"]
# Sort packages by priority
for registry_name in priority_order:
for package in packages:
if package.get("registry_name") == registry_name:
return package
# If no priority package found, return the first one
return packages[0] if packages else None

View File

@@ -0,0 +1,311 @@
"""VSCode implementation of MCP client adapter.
This adapter implements the VSCode-specific handling of MCP server configuration,
following the official documentation at:
https://code.visualstudio.com/docs/copilot/chat/mcp-servers
"""
import json
import os
from pathlib import Path
from .base import MCPClientAdapter
from ...registry.client import SimpleRegistryClient
from ...registry.integration import RegistryIntegration
class VSCodeClientAdapter(MCPClientAdapter):
"""VSCode implementation of MCP client adapter.
This adapter handles VSCode-specific configuration for MCP servers using
a repository-level .vscode/mcp.json file, following the format specified
in the VSCode documentation.
"""
def __init__(self, registry_url=None):
"""Initialize the VSCode client adapter.
Args:
registry_url (str, optional): URL of the MCP registry.
If not provided, uses the MCP_REGISTRY_URL environment variable
or falls back to the default demo registry.
"""
self.registry_client = SimpleRegistryClient(registry_url)
self.registry_integration = RegistryIntegration(registry_url)
def get_config_path(self):
"""Get the path to the VSCode MCP configuration file in the repository.
Returns:
str: Path to the .vscode/mcp.json file.
"""
# Use the current working directory as the repository root
repo_root = Path(os.getcwd())
# Path to .vscode/mcp.json in the repository
vscode_dir = repo_root / ".vscode"
mcp_config_path = vscode_dir / "mcp.json"
# Create the .vscode directory if it doesn't exist
try:
if not vscode_dir.exists():
vscode_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"Warning: Could not create .vscode directory: {e}")
return str(mcp_config_path)
def update_config(self, new_config):
"""Update the VSCode MCP configuration with new values.
Args:
new_config (dict): Complete configuration object to write.
Returns:
bool: True if successful, False otherwise.
"""
config_path = self.get_config_path()
try:
# Write the updated config
with open(config_path, "w", encoding="utf-8") as f:
json.dump(new_config, f, indent=2)
return True
except Exception as e:
print(f"Error updating VSCode MCP configuration: {e}")
return False
def get_current_config(self):
"""Get the current VSCode MCP configuration.
Returns:
dict: Current VSCode MCP configuration from the local .vscode/mcp.json file.
"""
config_path = self.get_config_path()
try:
try:
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
return {}
except Exception as e:
print(f"Error reading VSCode MCP configuration: {e}")
return {}
def configure_mcp_server(self, server_url, server_name=None, enabled=True, env_overrides=None, server_info_cache=None, runtime_vars=None):
"""Configure an MCP server in VS Code mcp.json file.
This method updates the .vscode/mcp.json file to add or update
an MCP server configuration.
Args:
server_url (str): URL or identifier of the MCP server.
server_name (str, optional): Name of the server. Defaults to None.
enabled (bool, optional): Whether to enable the server. Defaults to True.
env_overrides (dict, optional): Environment variable overrides. Defaults to None.
server_info_cache (dict, optional): Pre-fetched server info to avoid duplicate registry calls.
Returns:
bool: True if successful, False otherwise.
Raises:
ValueError: If server is not found in registry.
"""
if not server_url:
print("Error: server_url cannot be empty")
return False
try:
# Use cached server info if available, otherwise fetch from registry
if server_info_cache and server_url in server_info_cache:
server_info = server_info_cache[server_url]
else:
# Fallback to registry lookup if not cached
server_info = self.registry_client.find_server_by_reference(server_url)
# Fail if server is not found in registry - security requirement
# This raises ValueError as expected by tests
if not server_info:
raise ValueError(f"Failed to retrieve server details for '{server_url}'. Server not found in registry.")
# Generate server configuration
server_config, input_vars = self._format_server_config(server_info)
if not server_config:
print(f"Unable to configure server: {server_url}")
return False
# Use provided server name or fallback to server_url
config_key = server_name or server_url
# Get current config
current_config = self.get_current_config()
# Ensure servers and inputs sections exist
if "servers" not in current_config:
current_config["servers"] = {}
if "inputs" not in current_config:
current_config["inputs"] = []
# Add the server configuration
current_config["servers"][config_key] = server_config
# Add input variables (avoiding duplicates)
existing_input_ids = {var.get("id") for var in current_config["inputs"] if isinstance(var, dict)}
for var in input_vars:
if var.get("id") not in existing_input_ids:
current_config["inputs"].append(var)
existing_input_ids.add(var.get("id"))
# Update the configuration
result = self.update_config(current_config)
if result:
print(f"Successfully configured MCP server '{config_key}' for VS Code")
return result
except ValueError:
# Re-raise ValueError for registry errors
raise
except Exception as e:
print(f"Error configuring MCP server: {e}")
return False
def _format_server_config(self, server_info):
"""Format server details into VSCode mcp.json compatible format.
Args:
server_info (dict): Server information from registry.
Returns:
tuple: (server_config, input_vars) where:
- server_config is the formatted server configuration for mcp.json
- input_vars is a list of input variable definitions
"""
# Initialize the base config structure
server_config = {}
input_vars = []
# Check for packages information
if "packages" in server_info and server_info["packages"]:
package = server_info["packages"][0]
runtime_hint = package.get("runtime_hint", "")
# Handle npm packages
if runtime_hint == "npx" or "npm" in package.get("registry_name", "").lower():
# Get args directly from runtime_arguments
args = []
if "runtime_arguments" in package and package["runtime_arguments"]:
for arg in package["runtime_arguments"]:
if arg.get("is_required", False) and arg.get("value_hint"):
args.append(arg.get("value_hint"))
# Fallback if no runtime_arguments are provided
if not args and package.get("name"):
args = [package.get("name")]
server_config = {
"type": "stdio",
"command": "npx",
"args": args
}
# Handle docker packages
elif runtime_hint == "docker":
# Get args directly from runtime_arguments
args = []
if "runtime_arguments" in package and package["runtime_arguments"]:
for arg in package["runtime_arguments"]:
if arg.get("is_required", False) and arg.get("value_hint"):
args.append(arg.get("value_hint"))
# Fallback if no runtime_arguments are provided - use standard docker run command
if not args:
args = ["run", "-i", "--rm", package.get("name")]
server_config = {
"type": "stdio",
"command": "docker",
"args": args
}
# Handle Python packages
elif runtime_hint in ["uvx", "pip", "python"] or "python" in runtime_hint or package.get("registry_name", "").lower() == "pypi":
# Determine the command based on runtime_hint
if runtime_hint == "uvx":
command = "uvx"
elif "python" in runtime_hint:
# Use the specified Python path if it's a full path, otherwise default to python3
command = "python3" if runtime_hint in ["python", "pip"] else runtime_hint
else:
command = "python3"
# Get args directly from runtime_arguments
args = []
if "runtime_arguments" in package and package["runtime_arguments"]:
for arg in package["runtime_arguments"]:
if arg.get("is_required", False) and arg.get("value_hint"):
args.append(arg.get("value_hint"))
# Fallback if no runtime_arguments are provided
if not args:
if runtime_hint == "uvx":
module_name = package.get("name", "").replace("mcp-server-", "")
args = [f"mcp-server-{module_name}"]
else:
module_name = package.get("name", "").replace("mcp-server-", "").replace("-", "_")
args = ["-m", f"mcp_server_{module_name}"]
server_config = {
"type": "stdio",
"command": command,
"args": args
}
# Add environment variables if present
if "environment_variables" in package and package["environment_variables"]:
server_config["env"] = {}
for env_var in package["environment_variables"]:
if "name" in env_var:
# Convert variable name to lowercase and replace underscores with hyphens for VS Code convention
input_var_name = env_var["name"].lower().replace("_", "-")
# Create the input variable reference
server_config["env"][env_var["name"]] = f"${{input:{input_var_name}}}"
# Create the input variable definition
input_var_def = {
"type": "promptString",
"id": input_var_name,
"description": env_var.get("description", f"{env_var['name']} for MCP server"),
"password": True # Default to True for security
}
input_vars.append(input_var_def)
# If no server config was created from packages, check for other server types
if not server_config:
# Check for SSE endpoints
if "sse_endpoint" in server_info:
server_config = {
"type": "sse",
"url": server_info["sse_endpoint"],
"headers": server_info.get("sse_headers", {})
}
# Check for remotes (similar to Copilot adapter)
elif "remotes" in server_info and server_info["remotes"]:
remotes = server_info["remotes"]
remote = remotes[0] # Take the first remote
if remote.get("transport_type") == "sse":
server_config = {
"type": "sse",
"url": remote.get("url", ""),
"headers": remote.get("headers", {})
}
# If no packages AND no endpoints/remotes, fail with clear error
else:
raise ValueError(f"MCP server has incomplete configuration in registry - no package information or remote endpoints available. "
f"This appears to be a temporary registry issue. "
f"Server: {server_info.get('name', 'unknown')}")
return server_config, input_vars

View File

@@ -0,0 +1 @@
"""Package manager adapters package."""

View File

@@ -0,0 +1,27 @@
"""Base adapter interface for MCP package managers."""
from abc import ABC, abstractmethod
class MCPPackageManagerAdapter(ABC):
"""Base adapter for MCP package managers."""
@abstractmethod
def install(self, package_name, version=None):
"""Install an MCP package."""
pass
@abstractmethod
def uninstall(self, package_name):
"""Uninstall an MCP package."""
pass
@abstractmethod
def list_installed(self):
"""List all installed MCP packages."""
pass
@abstractmethod
def search(self, query):
"""Search for MCP packages."""
pass

View File

@@ -0,0 +1,123 @@
"""Implementation of the default MCP package manager."""
from .base import MCPPackageManagerAdapter
from ...config import get_default_client
from ...registry.integration import RegistryIntegration
class DefaultMCPPackageManager(MCPPackageManagerAdapter):
"""Implementation of the default MCP package manager."""
def install(self, package_name, version=None):
"""Install an MCP package.
Args:
package_name (str): Name of the package to install.
version (str, optional): Version of the package to install.
Returns:
bool: True if successful, False otherwise.
"""
try:
# Import here to avoid circular import
from ...factory import ClientFactory
client_type = get_default_client()
client_adapter = ClientFactory.create_client(client_type)
# For VSCode, configure MCP server in mcp.json
result = client_adapter.configure_mcp_server(package_name, package_name, True)
if result:
print(f"Successfully installed {package_name}")
return result
except Exception as e:
print(f"Error installing package {package_name}: {e}")
return False
def uninstall(self, package_name):
"""Uninstall an MCP package.
Args:
package_name (str): Name of the package to uninstall.
Returns:
bool: True if successful, False otherwise.
"""
try:
# Import here to avoid circular import
from ...factory import ClientFactory
client_type = get_default_client()
client_adapter = ClientFactory.create_client(client_type)
config = client_adapter.get_current_config()
# For VSCode, remove the server from mcp.json
if "servers" in config and package_name in config["servers"]:
servers = config["servers"]
servers.pop(package_name, None)
result = client_adapter.update_config({"servers": servers})
if result:
print(f"Successfully uninstalled {package_name}")
return result
else:
print(f"Package {package_name} not found in configuration")
return False
except Exception as e:
print(f"Error uninstalling package {package_name}: {e}")
return False
def list_installed(self):
"""List all installed MCP packages.
Returns:
list: List of installed packages.
"""
try:
# Import here to avoid circular import
from ...factory import ClientFactory
# Get client type from configuration (default is vscode)
client_type = get_default_client()
# Create client adapter
client_adapter = ClientFactory.create_client(client_type)
# Get config from local .vscode/mcp.json file
config = client_adapter.get_current_config()
# Extract server names from the config
servers = config.get("servers", {})
# Return the list of server names
return list(servers.keys())
except Exception as e:
print(f"Error retrieving installed MCP servers: {e}")
return []
def search(self, query):
"""Search for MCP packages.
Args:
query (str): Search query.
Returns:
list: List of packages matching the query.
"""
try:
# Use the registry integration to search for packages
registry = RegistryIntegration()
packages = registry.search_packages(query)
# Return the list of package IDs/names
return [pkg.get("id", pkg.get("name", "Unknown")) for pkg in packages] if packages else []
except Exception as e:
print(f"Error searching for packages: {e}")
return []

2555
src/apm_cli/cli.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
"""Commands package for APM CLI."""
from .deps import deps
__all__ = ['deps']

View File

@@ -0,0 +1,656 @@
"""APM dependency management commands."""
import sys
import shutil
import click
from pathlib import Path
from typing import List, Optional, Dict, Any
# Import existing APM components
from ..models.apm_package import APMPackage, ValidationResult, validate_apm_package
from ..utils.console import _rich_success, _rich_error, _rich_info, _rich_warning
# Import APM dependency system components (with fallback)
from ..deps.github_downloader import GitHubPackageDownloader
from ..deps.apm_resolver import APMDependencyResolver
@click.group(help="🔗 Manage APM package dependencies")
def deps():
"""APM dependency management commands."""
pass
@deps.command(name="list", help="📋 List installed APM dependencies")
def list_packages():
"""Show all installed APM dependencies with context files and agent workflows."""
try:
# Import Rich components with fallback
from rich.table import Table
from rich.console import Console
console = Console()
has_rich = True
except ImportError:
has_rich = False
console = None
try:
project_root = Path(".")
apm_modules_path = project_root / "apm_modules"
# Check if apm_modules exists
if not apm_modules_path.exists():
if has_rich:
console.print("💡 No APM dependencies installed yet", style="cyan")
console.print("Run 'specify apm install' to install dependencies from apm.yml", style="dim")
else:
click.echo("💡 No APM dependencies installed yet")
click.echo("Run 'specify apm install' to install dependencies from apm.yml")
return
# Load project dependencies to check for orphaned packages
declared_deps = set()
try:
apm_yml_path = project_root / "apm.yml"
if apm_yml_path.exists():
project_package = APMPackage.from_apm_yml(apm_yml_path)
for dep in project_package.get_apm_dependencies():
declared_deps.add(dep.repo_url)
except Exception:
pass # Continue without orphan detection if apm.yml parsing fails
# Scan for installed packages in org-namespaced structure
installed_packages = []
orphaned_packages = []
for org_dir in apm_modules_path.iterdir():
if org_dir.is_dir() and not org_dir.name.startswith('.'):
for package_dir in org_dir.iterdir():
if package_dir.is_dir() and not package_dir.name.startswith('.'):
try:
# org/repo format
org_repo_name = f"{org_dir.name}/{package_dir.name}"
# Try to load package metadata
apm_yml_path = package_dir / "apm.yml"
if apm_yml_path.exists():
package = APMPackage.from_apm_yml(apm_yml_path)
# Count context files and workflows separately
context_count, workflow_count = _count_package_files(package_dir)
# Check if this package is orphaned
is_orphaned = org_repo_name not in declared_deps
if is_orphaned:
orphaned_packages.append(org_repo_name)
installed_packages.append({
'name': org_repo_name,
'version': package.version or 'unknown',
'source': 'orphaned' if is_orphaned else 'github',
'context': context_count,
'workflows': workflow_count,
'path': str(package_dir),
'is_orphaned': is_orphaned
})
else:
# Package without apm.yml - show basic info
context_count, workflow_count = _count_package_files(package_dir)
is_orphaned = True # Assume orphaned if no apm.yml
orphaned_packages.append(org_repo_name)
installed_packages.append({
'name': org_repo_name,
'version': 'unknown',
'source': 'orphaned',
'context': context_count,
'workflows': workflow_count,
'path': str(package_dir),
'is_orphaned': is_orphaned
})
except Exception as e:
click.echo(f"⚠️ Warning: Failed to read package {org_dir.name}/{package_dir.name}: {e}")
if not installed_packages:
if has_rich:
console.print("💡 apm_modules/ directory exists but contains no valid packages", style="cyan")
else:
click.echo("💡 apm_modules/ directory exists but contains no valid packages")
return
# Display packages in table format
if has_rich:
table = Table(title="📋 APM Dependencies", show_header=True, header_style="bold cyan")
table.add_column("Package", style="bold white")
table.add_column("Version", style="yellow")
table.add_column("Source", style="blue")
table.add_column("Context", style="green")
table.add_column("Workflows", style="magenta")
for pkg in installed_packages:
table.add_row(
pkg['name'],
pkg['version'],
pkg['source'],
f"{pkg['context']} files",
f"{pkg['workflows']} workflows"
)
console.print(table)
# Show orphaned packages warning
if orphaned_packages:
console.print(f"\n⚠️ {len(orphaned_packages)} orphaned package(s) found (not in apm.yml):", style="yellow")
for pkg in orphaned_packages:
console.print(f"{pkg}", style="dim yellow")
console.print("\n💡 Run 'specify apm prune' to remove orphaned packages", style="cyan")
else:
# Fallback text table
click.echo("📋 APM Dependencies:")
click.echo("┌─────────────────────┬─────────┬──────────────┬─────────────┬─────────────┐")
click.echo("│ Package │ Version │ Source │ Context │ Workflows │")
click.echo("├─────────────────────┼─────────┼──────────────┼─────────────┼─────────────┤")
for pkg in installed_packages:
name = pkg['name'][:19].ljust(19)
version = pkg['version'][:7].ljust(7)
source = pkg['source'][:12].ljust(12)
context = f"{pkg['context']} files".ljust(11)
workflows = f"{pkg['workflows']} wf".ljust(11)
click.echo(f"{name}{version}{source}{context}{workflows}")
click.echo("└─────────────────────┴─────────┴──────────────┴─────────────┴─────────────┘")
# Show orphaned packages warning
if orphaned_packages:
click.echo(f"\n⚠️ {len(orphaned_packages)} orphaned package(s) found (not in apm.yml):")
for pkg in orphaned_packages:
click.echo(f"{pkg}")
click.echo("\n💡 Run 'specify apm prune' to remove orphaned packages")
except Exception as e:
_rich_error(f"Error listing dependencies: {e}")
sys.exit(1)
@deps.command(help="🌳 Show dependency tree structure")
def tree():
"""Display dependencies in hierarchical tree format showing context and workflows."""
try:
# Import Rich components with fallback
from rich.tree import Tree
from rich.console import Console
console = Console()
has_rich = True
except ImportError:
has_rich = False
console = None
try:
project_root = Path(".")
apm_modules_path = project_root / "apm_modules"
# Load project info
project_name = "my-project"
try:
apm_yml_path = project_root / "apm.yml"
if apm_yml_path.exists():
root_package = APMPackage.from_apm_yml(apm_yml_path)
project_name = root_package.name
except Exception:
pass
if has_rich:
# Create Rich tree
root_tree = Tree(f"[bold cyan]{project_name}[/bold cyan] (local)")
# Check if apm_modules exists
if not apm_modules_path.exists():
root_tree.add("[dim]No dependencies installed[/dim]")
else:
# Add each dependency as a branch
for package_dir in apm_modules_path.iterdir():
if package_dir.is_dir():
try:
package_info = _get_package_display_info(package_dir)
branch = root_tree.add(f"[green]{package_info['display_name']}[/green]")
# Add context files and workflows as sub-items
context_files = _get_detailed_context_counts(package_dir)
workflow_count = _count_workflows(package_dir)
# Show context files by type
for context_type, count in context_files.items():
if count > 0:
branch.add(f"[dim]{count} {context_type}[/dim]")
# Show workflows
if workflow_count > 0:
branch.add(f"[bold magenta]{workflow_count} agent workflows[/bold magenta]")
if not any(count > 0 for count in context_files.values()) and workflow_count == 0:
branch.add("[dim]no context or workflows[/dim]")
except Exception as e:
branch = root_tree.add(f"[red]{package_dir.name}[/red] [dim](error loading)[/dim]")
console.print(root_tree)
else:
# Fallback text tree
click.echo(f"{project_name} (local)")
if not apm_modules_path.exists():
click.echo("└── No dependencies installed")
return
package_dirs = [d for d in apm_modules_path.iterdir() if d.is_dir()]
for i, package_dir in enumerate(package_dirs):
is_last = i == len(package_dirs) - 1
prefix = "└── " if is_last else "├── "
try:
package_info = _get_package_display_info(package_dir)
click.echo(f"{prefix}{package_info['display_name']}")
# Add context files and workflows
context_files = _get_detailed_context_counts(package_dir)
workflow_count = _count_workflows(package_dir)
sub_prefix = " " if is_last else ""
items_shown = False
for context_type, count in context_files.items():
if count > 0:
click.echo(f"{sub_prefix}├── {count} {context_type}")
items_shown = True
if workflow_count > 0:
click.echo(f"{sub_prefix}├── {workflow_count} agent workflows")
items_shown = True
if not items_shown:
click.echo(f"{sub_prefix}└── no context or workflows")
except Exception as e:
click.echo(f"{prefix}{package_dir.name} (error loading)")
except Exception as e:
_rich_error(f"Error showing dependency tree: {e}")
sys.exit(1)
@deps.command(help="🧹 Remove all APM dependencies")
def clean():
"""Remove entire apm_modules/ directory."""
project_root = Path(".")
apm_modules_path = project_root / "apm_modules"
if not apm_modules_path.exists():
_rich_info("No apm_modules/ directory found - already clean")
return
# Show what will be removed
package_count = len([d for d in apm_modules_path.iterdir() if d.is_dir()])
_rich_warning(f"This will remove the entire apm_modules/ directory ({package_count} packages)")
# Confirmation prompt
try:
from rich.prompt import Confirm
confirm = Confirm.ask("Continue?")
except ImportError:
confirm = click.confirm("Continue?")
if not confirm:
_rich_info("Operation cancelled")
return
try:
shutil.rmtree(apm_modules_path)
_rich_success("Successfully removed apm_modules/ directory")
except Exception as e:
_rich_error(f"Error removing apm_modules/: {e}")
sys.exit(1)
@deps.command(help="🔄 Update APM dependencies")
@click.argument('package', required=False)
def update(package: Optional[str]):
"""Update specific package or all if no package specified."""
project_root = Path(".")
apm_modules_path = project_root / "apm_modules"
if not apm_modules_path.exists():
_rich_info("No apm_modules/ directory found - no packages to update")
return
# Get project dependencies to validate updates
try:
apm_yml_path = project_root / "apm.yml"
if not apm_yml_path.exists():
_rich_error("No apm.yml found in current directory")
return
project_package = APMPackage.from_apm_yml(apm_yml_path)
project_deps = project_package.get_apm_dependencies()
if not project_deps:
_rich_info("No APM dependencies defined in apm.yml")
return
except Exception as e:
_rich_error(f"Error reading apm.yml: {e}")
return
if package:
# Update specific package
_update_single_package(package, project_deps, apm_modules_path)
else:
# Update all packages
_update_all_packages(project_deps, apm_modules_path)
@deps.command(help=" Show detailed package information")
@click.argument('package', required=True)
def info(package: str):
"""Show detailed information about a specific package including context files and workflows."""
project_root = Path(".")
apm_modules_path = project_root / "apm_modules"
if not apm_modules_path.exists():
_rich_error("No apm_modules/ directory found")
_rich_info("Run 'specify apm install' to install dependencies first")
sys.exit(1)
# Find the package directory
package_path = None
for package_dir in apm_modules_path.iterdir():
if package_dir.is_dir() and package_dir.name == package:
package_path = package_dir
break
if not package_path:
_rich_error(f"Package '{package}' not found in apm_modules/")
_rich_info("Available packages:")
for package_dir in apm_modules_path.iterdir():
if package_dir.is_dir():
click.echo(f" - {package_dir.name}")
sys.exit(1)
try:
# Load package information
package_info = _get_detailed_package_info(package_path)
# Display with Rich panel if available
try:
from rich.panel import Panel
from rich.console import Console
from rich.text import Text
console = Console()
content_lines = []
content_lines.append(f"[bold]Name:[/bold] {package_info['name']}")
content_lines.append(f"[bold]Version:[/bold] {package_info['version']}")
content_lines.append(f"[bold]Description:[/bold] {package_info['description']}")
content_lines.append(f"[bold]Author:[/bold] {package_info['author']}")
content_lines.append(f"[bold]Source:[/bold] {package_info['source']}")
content_lines.append(f"[bold]Install Path:[/bold] {package_info['install_path']}")
content_lines.append("")
content_lines.append("[bold]Context Files:[/bold]")
for context_type, count in package_info['context_files'].items():
if count > 0:
content_lines.append(f"{count} {context_type}")
if not any(count > 0 for count in package_info['context_files'].values()):
content_lines.append(" • No context files found")
content_lines.append("")
content_lines.append("[bold]Agent Workflows:[/bold]")
if package_info['workflows'] > 0:
content_lines.append(f"{package_info['workflows']} executable workflows")
else:
content_lines.append(" • No agent workflows found")
content = "\n".join(content_lines)
panel = Panel(content, title=f" Package Info: {package}", border_style="cyan")
console.print(panel)
except ImportError:
# Fallback text display
click.echo(f" Package Info: {package}")
click.echo("=" * 40)
click.echo(f"Name: {package_info['name']}")
click.echo(f"Version: {package_info['version']}")
click.echo(f"Description: {package_info['description']}")
click.echo(f"Author: {package_info['author']}")
click.echo(f"Source: {package_info['source']}")
click.echo(f"Install Path: {package_info['install_path']}")
click.echo("")
click.echo("Context Files:")
for context_type, count in package_info['context_files'].items():
if count > 0:
click.echo(f"{count} {context_type}")
if not any(count > 0 for count in package_info['context_files'].values()):
click.echo(" • No context files found")
click.echo("")
click.echo("Agent Workflows:")
if package_info['workflows'] > 0:
click.echo(f"{package_info['workflows']} executable workflows")
else:
click.echo(" • No agent workflows found")
except Exception as e:
_rich_error(f"Error reading package information: {e}")
sys.exit(1)
# Helper functions
def _count_package_files(package_path: Path) -> tuple[int, int]:
"""Count context files and workflows in a package.
Returns:
tuple: (context_count, workflow_count)
"""
apm_dir = package_path / ".apm"
if not apm_dir.exists():
# Also check root directory for .prompt.md files
workflow_count = len(list(package_path.glob("*.prompt.md")))
return 0, workflow_count
context_count = 0
context_dirs = ['instructions', 'chatmodes', 'contexts']
for context_dir in context_dirs:
context_path = apm_dir / context_dir
if context_path.exists() and context_path.is_dir():
context_count += len(list(context_path.glob("*.md")))
# Count workflows in both .apm/prompts and root directory
workflow_count = 0
prompts_path = apm_dir / "prompts"
if prompts_path.exists() and prompts_path.is_dir():
workflow_count += len(list(prompts_path.glob("*.prompt.md")))
# Also check root directory for .prompt.md files
workflow_count += len(list(package_path.glob("*.prompt.md")))
return context_count, workflow_count
def _count_workflows(package_path: Path) -> int:
"""Count agent workflows (.prompt.md files) in a package."""
_, workflow_count = _count_package_files(package_path)
return workflow_count
def _get_detailed_context_counts(package_path: Path) -> Dict[str, int]:
"""Get detailed context file counts by type."""
apm_dir = package_path / ".apm"
if not apm_dir.exists():
return {'instructions': 0, 'chatmodes': 0, 'contexts': 0}
counts = {}
context_types = {
'instructions': ['instructions.md'],
'chatmodes': ['chatmode.md'],
'contexts': ['context.md', 'memory.md']
}
for context_type, extensions in context_types.items():
count = 0
context_path = apm_dir / context_type
if context_path.exists() and context_path.is_dir():
for ext in extensions:
count += len(list(context_path.glob(f"*.{ext}")))
counts[context_type] = count
return counts
def _get_package_display_info(package_path: Path) -> Dict[str, str]:
"""Get package display information."""
try:
apm_yml_path = package_path / "apm.yml"
if apm_yml_path.exists():
package = APMPackage.from_apm_yml(apm_yml_path)
version_info = f"@{package.version}" if package.version else "@unknown"
return {
'display_name': f"{package.name}{version_info}",
'name': package.name,
'version': package.version or 'unknown'
}
else:
return {
'display_name': f"{package_path.name}@unknown",
'name': package_path.name,
'version': 'unknown'
}
except Exception:
return {
'display_name': f"{package_path.name}@error",
'name': package_path.name,
'version': 'error'
}
def _get_detailed_package_info(package_path: Path) -> Dict[str, Any]:
"""Get detailed package information for the info command."""
try:
apm_yml_path = package_path / "apm.yml"
if apm_yml_path.exists():
package = APMPackage.from_apm_yml(apm_yml_path)
context_count, workflow_count = _count_package_files(package_path)
return {
'name': package.name,
'version': package.version or 'unknown',
'description': package.description or 'No description',
'author': package.author or 'Unknown',
'source': package.source or 'local',
'install_path': str(package_path.resolve()),
'context_files': _get_detailed_context_counts(package_path),
'workflows': workflow_count
}
else:
context_count, workflow_count = _count_package_files(package_path)
return {
'name': package_path.name,
'version': 'unknown',
'description': 'No apm.yml found',
'author': 'Unknown',
'source': 'unknown',
'install_path': str(package_path.resolve()),
'context_files': _get_detailed_context_counts(package_path),
'workflows': workflow_count
}
except Exception as e:
return {
'name': package_path.name,
'version': 'error',
'description': f'Error loading package: {e}',
'author': 'Unknown',
'source': 'unknown',
'install_path': str(package_path.resolve()),
'context_files': {'instructions': 0, 'chatmodes': 0, 'contexts': 0},
'workflows': 0
}
def _update_single_package(package_name: str, project_deps: List, apm_modules_path: Path):
"""Update a specific package."""
# Find the dependency reference for this package
target_dep = None
for dep in project_deps:
if dep.get_display_name() == package_name or dep.repo_url.split('/')[-1] == package_name:
target_dep = dep
break
if not target_dep:
_rich_error(f"Package '{package_name}' not found in apm.yml dependencies")
return
# Find the installed package directory
package_dir = None
if target_dep.alias:
package_dir = apm_modules_path / target_dep.alias
else:
package_dir = apm_modules_path / package_name
if not package_dir.exists():
_rich_error(f"Package '{package_name}' not installed in apm_modules/")
_rich_info(f"Run 'apm install' to install it first")
return
try:
downloader = GitHubPackageDownloader()
_rich_info(f"Updating {target_dep.repo_url}...")
# Download latest version
package_info = downloader.download_package(str(target_dep), package_dir)
_rich_success(f"✅ Updated {target_dep.repo_url}")
except Exception as e:
_rich_error(f"Failed to update {package_name}: {e}")
def _update_all_packages(project_deps: List, apm_modules_path: Path):
"""Update all packages."""
if not project_deps:
_rich_info("No APM dependencies to update")
return
_rich_info(f"Updating {len(project_deps)} APM dependencies...")
downloader = GitHubPackageDownloader()
updated_count = 0
for dep in project_deps:
# Determine package directory
if dep.alias:
package_dir = apm_modules_path / dep.alias
else:
package_dir = apm_modules_path / dep.repo_url.split('/')[-1]
if not package_dir.exists():
_rich_warning(f"⚠️ {dep.repo_url} not installed - skipping")
continue
try:
_rich_info(f" Updating {dep.repo_url}...")
package_info = downloader.download_package(str(dep), package_dir)
updated_count += 1
_rich_success(f"{dep.repo_url}")
except Exception as e:
_rich_error(f" ❌ Failed to update {dep.repo_url}: {e}")
continue
_rich_success(f"Updated {updated_count} of {len(project_deps)} packages")

View File

@@ -0,0 +1,29 @@
"""APM compilation module for generating AGENTS.md files."""
from .agents_compiler import AgentsCompiler, compile_agents_md, CompilationConfig, CompilationResult
from .template_builder import (
build_conditional_sections,
TemplateData,
find_chatmode_by_name
)
from .link_resolver import (
resolve_markdown_links,
validate_link_targets
)
__all__ = [
# Main compilation interface
'AgentsCompiler',
'compile_agents_md',
'CompilationConfig',
'CompilationResult',
# Template building
'build_conditional_sections',
'TemplateData',
'find_chatmode_by_name',
# Link resolution
'resolve_markdown_links',
'validate_link_targets'
]

View File

@@ -0,0 +1,630 @@
"""Main compilation orchestration for AGENTS.md generation.
Timestamp generation removed in favor of deterministic Build ID handled after
full content assembly. This keeps repeated compiles byte-identical when source
primitives & constitution are unchanged.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Dict, Any
from ..primitives.models import PrimitiveCollection
from ..primitives.discovery import discover_primitives
from ..version import get_version
from .template_builder import (
build_conditional_sections,
generate_agents_md_template,
TemplateData,
find_chatmode_by_name
)
from .link_resolver import resolve_markdown_links, validate_link_targets
@dataclass
class CompilationConfig:
"""Configuration for AGENTS.md compilation."""
output_path: str = "AGENTS.md"
chatmode: Optional[str] = None
resolve_links: bool = True
dry_run: bool = False
with_constitution: bool = True # Phase 0 feature flag
# Distributed compilation settings (Task 7)
strategy: str = "distributed" # "distributed" or "single-file"
single_agents: bool = False # Force single-file mode
trace: bool = False # Show source attribution and conflicts
local_only: bool = False # Ignore dependencies, compile only local primitives
debug: bool = False # Show context optimizer analysis and metrics
min_instructions_per_file: int = 1 # Minimum instructions per AGENTS.md file (Minimal Context Principle)
source_attribution: bool = True # Include source file comments
clean_orphaned: bool = False # Remove orphaned AGENTS.md files
def __post_init__(self):
"""Handle CLI flag precedence after initialization."""
if self.single_agents:
self.strategy = "single-file"
@classmethod
def from_apm_yml(cls, **overrides) -> 'CompilationConfig':
"""Create configuration from apm.yml with command-line overrides.
Args:
**overrides: Command-line arguments that override config file values.
Returns:
CompilationConfig: Configuration with apm.yml values and overrides applied.
"""
config = cls()
# Try to load from apm.yml
try:
from pathlib import Path
import yaml
if Path('apm.yml').exists():
with open('apm.yml', 'r') as f:
apm_config = yaml.safe_load(f) or {}
# Look for compilation section
compilation_config = apm_config.get('compilation', {})
# Apply config file values
if 'output' in compilation_config:
config.output_path = compilation_config['output']
if 'chatmode' in compilation_config:
config.chatmode = compilation_config['chatmode']
if 'resolve_links' in compilation_config:
config.resolve_links = compilation_config['resolve_links']
# Distributed compilation settings (Task 7)
if 'strategy' in compilation_config:
config.strategy = compilation_config['strategy']
if 'single_file' in compilation_config:
# Legacy config support - if single_file is True, override strategy
if compilation_config['single_file']:
config.strategy = "single-file"
config.single_agents = True
# Placement settings
placement_config = compilation_config.get('placement', {})
if 'min_instructions_per_file' in placement_config:
config.min_instructions_per_file = placement_config['min_instructions_per_file']
# Source attribution
if 'source_attribution' in compilation_config:
config.source_attribution = compilation_config['source_attribution']
except Exception:
# If config loading fails, use defaults
pass
# Apply command-line overrides (highest priority)
for key, value in overrides.items():
if value is not None: # Only override if explicitly provided
setattr(config, key, value)
# Handle CLI flag precedence
if config.single_agents:
config.strategy = "single-file"
return config
@dataclass
class CompilationResult:
"""Result of AGENTS.md compilation."""
success: bool
output_path: str
content: str
warnings: List[str]
errors: List[str]
stats: Dict[str, Any]
class AgentsCompiler:
"""Main compiler for generating AGENTS.md files."""
def __init__(self, base_dir: str = "."):
"""Initialize the compiler.
Args:
base_dir (str): Base directory for compilation. Defaults to current directory.
"""
self.base_dir = Path(base_dir)
self.warnings: List[str] = []
self.errors: List[str] = []
def compile(self, config: CompilationConfig, primitives: Optional[PrimitiveCollection] = None) -> CompilationResult:
"""Compile AGENTS.md with the given configuration.
Args:
config (CompilationConfig): Compilation configuration.
primitives (Optional[PrimitiveCollection]): Primitives to use, or None to discover.
Returns:
CompilationResult: Result of the compilation.
"""
self.warnings.clear()
self.errors.clear()
try:
# Use provided primitives or discover them (with dependency support)
if primitives is None:
if config.local_only:
# Use basic discovery for local-only mode
primitives = discover_primitives(str(self.base_dir))
else:
# Use enhanced discovery with dependencies (Task 4 integration)
from ..primitives.discovery import discover_primitives_with_dependencies
primitives = discover_primitives_with_dependencies(str(self.base_dir))
# Handle distributed compilation (Task 7 - new default behavior)
if config.strategy == "distributed" and not config.single_agents:
return self._compile_distributed(config, primitives)
else:
# Traditional single-file compilation (backward compatibility)
return self._compile_single_file(config, primitives)
except Exception as e:
self.errors.append(f"Compilation failed: {str(e)}")
return CompilationResult(
success=False,
output_path="",
content="",
warnings=self.warnings.copy(),
errors=self.errors.copy(),
stats={}
)
def _compile_distributed(self, config: CompilationConfig, primitives: PrimitiveCollection) -> CompilationResult:
"""Compile using distributed AGENTS.md approach (Task 7).
Args:
config (CompilationConfig): Compilation configuration.
primitives (PrimitiveCollection): Primitives to compile.
Returns:
CompilationResult: Result of distributed compilation.
"""
from .distributed_compiler import DistributedAgentsCompiler
# Create distributed compiler
distributed_compiler = DistributedAgentsCompiler(str(self.base_dir))
# Prepare configuration for distributed compilation
distributed_config = {
'min_instructions_per_file': config.min_instructions_per_file,
# max_depth removed - full project analysis
'source_attribution': config.source_attribution,
'debug': config.debug,
'clean_orphaned': config.clean_orphaned,
'dry_run': config.dry_run
}
# Compile distributed
distributed_result = distributed_compiler.compile_distributed(primitives, distributed_config)
# Display professional compilation output (always show, not just in debug)
compilation_results = distributed_compiler.get_compilation_results_for_display(config.dry_run)
if compilation_results:
if config.debug or config.trace:
# Verbose mode with mathematical analysis
output = distributed_compiler.output_formatter.format_verbose(compilation_results)
elif config.dry_run:
# Dry run mode with placement preview
output = distributed_compiler.output_formatter.format_dry_run(compilation_results)
else:
# Default mode with essential information
output = distributed_compiler.output_formatter.format_default(compilation_results)
# Display the professional output
print(output)
if not distributed_result.success:
self.warnings.extend(distributed_result.warnings)
self.errors.extend(distributed_result.errors)
return CompilationResult(
success=False,
output_path="",
content="",
warnings=self.warnings.copy(),
errors=self.errors.copy(),
stats=distributed_result.stats
)
# Handle dry-run mode (preview placement without writing files)
if config.dry_run:
# Count files that would be written (directories that exist)
successful_writes = 0
for agents_path in distributed_result.content_map.keys():
if agents_path.parent.exists():
successful_writes += 1
# Update stats with actual files that would be written
if distributed_result.stats:
distributed_result.stats["agents_files_generated"] = successful_writes
# Don't write files in preview mode - output already shown above
return CompilationResult(
success=True,
output_path="Preview mode - no files written",
content=self._generate_placement_summary(distributed_result),
warnings=distributed_result.warnings,
errors=distributed_result.errors,
stats=distributed_result.stats
)
# Write distributed AGENTS.md files
successful_writes = 0
total_content_entries = len(distributed_result.content_map)
for agents_path, content in distributed_result.content_map.items():
try:
self._write_distributed_file(agents_path, content, config)
successful_writes += 1
except OSError as e:
self.errors.append(f"Failed to write {agents_path}: {str(e)}")
# Update stats with actual files written
if distributed_result.stats:
distributed_result.stats["agents_files_generated"] = successful_writes
# Merge warnings and errors
self.warnings.extend(distributed_result.warnings)
self.errors.extend(distributed_result.errors)
# Create summary for backward compatibility
summary_content = self._generate_distributed_summary(distributed_result, config)
return CompilationResult(
success=len(self.errors) == 0,
output_path=f"Distributed: {len(distributed_result.placements)} AGENTS.md files",
content=summary_content,
warnings=self.warnings.copy(),
errors=self.errors.copy(),
stats=distributed_result.stats
)
def _compile_single_file(self, config: CompilationConfig, primitives: PrimitiveCollection) -> CompilationResult:
"""Compile using traditional single-file approach (backward compatibility).
Args:
config (CompilationConfig): Compilation configuration.
primitives (PrimitiveCollection): Primitives to compile.
Returns:
CompilationResult: Result of single-file compilation.
"""
# Validate primitives
validation_errors = self.validate_primitives(primitives)
if validation_errors:
self.errors.extend(validation_errors)
# Generate template data
template_data = self._generate_template_data(primitives, config)
# Generate final output
content = self.generate_output(template_data, config)
# Write output file (constitution injection handled externally in CLI)
output_path = str(self.base_dir / config.output_path)
if not config.dry_run:
self._write_output_file(output_path, content)
# Compile statistics
stats = self._compile_stats(primitives, template_data)
return CompilationResult(
success=len(self.errors) == 0,
output_path=output_path,
content=content,
warnings=self.warnings.copy(),
errors=self.errors.copy(),
stats=stats
)
def validate_primitives(self, primitives: PrimitiveCollection) -> List[str]:
"""Validate primitives for compilation.
Args:
primitives (PrimitiveCollection): Collection of primitives to validate.
Returns:
List[str]: List of validation errors.
"""
errors = []
# Validate each primitive
for primitive in primitives.all_primitives():
primitive_errors = primitive.validate()
if primitive_errors:
try:
# Try to get relative path, but fall back to absolute if it fails
file_path = str(primitive.file_path.relative_to(self.base_dir))
except ValueError:
# File is outside base_dir, use absolute path
file_path = str(primitive.file_path)
for error in primitive_errors:
# Treat validation errors as warnings instead of hard errors
# This allows compilation to continue with incomplete primitives
self.warnings.append(f"{file_path}: {error}")
# Validate markdown links in each primitive's content using its own directory as base
if hasattr(primitive, 'content') and primitive.content:
primitive_dir = primitive.file_path.parent
link_errors = validate_link_targets(primitive.content, primitive_dir)
if link_errors:
try:
file_path = str(primitive.file_path.relative_to(self.base_dir))
except ValueError:
file_path = str(primitive.file_path)
for link_error in link_errors:
self.warnings.append(f"{file_path}: {link_error}")
return errors
def generate_output(self, template_data: TemplateData, config: CompilationConfig) -> str:
"""Generate the final AGENTS.md output.
Args:
template_data (TemplateData): Data for template generation.
config (CompilationConfig): Compilation configuration.
Returns:
str: Generated AGENTS.md content.
"""
content = generate_agents_md_template(template_data)
# Resolve markdown links if enabled
if config.resolve_links:
content = resolve_markdown_links(content, self.base_dir)
return content
def _generate_template_data(self, primitives: PrimitiveCollection, config: CompilationConfig) -> TemplateData:
"""Generate template data from primitives and configuration.
Args:
primitives (PrimitiveCollection): Discovered primitives.
config (CompilationConfig): Compilation configuration.
Returns:
TemplateData: Template data for generation.
"""
# Build instructions content
instructions_content = build_conditional_sections(primitives.instructions)
# Metadata (version only; timestamp intentionally omitted for determinism)
version = get_version()
# Handle chatmode content
chatmode_content = None
if config.chatmode:
chatmode = find_chatmode_by_name(primitives.chatmodes, config.chatmode)
if chatmode:
chatmode_content = chatmode.content
else:
self.warnings.append(f"Chatmode '{config.chatmode}' not found")
return TemplateData(
instructions_content=instructions_content,
version=version,
chatmode_content=chatmode_content
)
def _write_output_file(self, output_path: str, content: str) -> None:
"""Write the generated content to the output file.
Args:
output_path (str): Path to write the output.
content (str): Content to write.
"""
try:
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
except OSError as e:
self.errors.append(f"Failed to write output file {output_path}: {str(e)}")
def _compile_stats(self, primitives: PrimitiveCollection, template_data: TemplateData) -> Dict[str, Any]:
"""Compile statistics about the compilation.
Args:
primitives (PrimitiveCollection): Discovered primitives.
template_data (TemplateData): Generated template data.
Returns:
Dict[str, Any]: Compilation statistics.
"""
return {
"primitives_found": primitives.count(),
"chatmodes": len(primitives.chatmodes),
"instructions": len(primitives.instructions),
"contexts": len(primitives.contexts),
"content_length": len(template_data.instructions_content),
# timestamp removed
"version": template_data.version
}
def _write_distributed_file(self, agents_path: Path, content: str, config: CompilationConfig) -> None:
"""Write a distributed AGENTS.md file with constitution injection support.
Args:
agents_path (Path): Path to write the AGENTS.md file.
content (str): Content to write.
config (CompilationConfig): Compilation configuration.
"""
try:
# Handle constitution injection for distributed files
final_content = content
if config.with_constitution:
# Try to inject constitution if available
try:
from .injector import ConstitutionInjector
injector = ConstitutionInjector(str(agents_path.parent))
final_content, c_status, c_hash = injector.inject(
content,
with_constitution=True,
output_path=agents_path
)
except Exception:
# If constitution injection fails, use original content
pass
# Create directory if it doesn't exist
agents_path.parent.mkdir(parents=True, exist_ok=True)
# Write the file
with open(agents_path, 'w', encoding='utf-8') as f:
f.write(final_content)
except OSError as e:
raise OSError(f"Failed to write distributed AGENTS.md file {agents_path}: {str(e)}")
def _display_placement_preview(self, distributed_result) -> None:
"""Display placement preview for --show-placement mode.
Args:
distributed_result: Result from distributed compilation.
"""
print("🔍 Distributed AGENTS.md Placement Preview:")
print()
for placement in distributed_result.placements:
try:
rel_path = placement.agents_path.relative_to(self.base_dir.resolve())
except ValueError:
# Fallback for path resolution issues
rel_path = placement.agents_path
print(f"📄 {rel_path}")
print(f" Instructions: {len(placement.instructions)}")
print(f" Patterns: {', '.join(sorted(placement.coverage_patterns))}")
if placement.source_attribution:
sources = set(placement.source_attribution.values())
print(f" Sources: {', '.join(sorted(sources))}")
print()
def _display_trace_info(self, distributed_result, primitives: PrimitiveCollection) -> None:
"""Display detailed trace information for --trace mode.
Args:
distributed_result: Result from distributed compilation.
primitives (PrimitiveCollection): Full primitive collection.
"""
print("🔍 Distributed Compilation Trace:")
print()
for placement in distributed_result.placements:
try:
rel_path = placement.agents_path.relative_to(self.base_dir.resolve())
except ValueError:
rel_path = placement.agents_path
print(f"📄 {rel_path}")
for instruction in placement.instructions:
source = getattr(instruction, 'source', 'local')
try:
inst_path = instruction.file_path.relative_to(self.base_dir.resolve())
except ValueError:
inst_path = instruction.file_path
print(f"{instruction.apply_to or 'no pattern'} <- {source} {inst_path}")
print()
def _generate_placement_summary(self, distributed_result) -> str:
"""Generate a text summary of placement results.
Args:
distributed_result: Result from distributed compilation.
Returns:
str: Text summary of placements.
"""
lines = ["Distributed AGENTS.md Placement Summary:", ""]
for placement in distributed_result.placements:
try:
rel_path = placement.agents_path.relative_to(self.base_dir.resolve())
except ValueError:
rel_path = placement.agents_path
lines.append(f"📄 {rel_path}")
lines.append(f" Instructions: {len(placement.instructions)}")
lines.append(f" Patterns: {', '.join(sorted(placement.coverage_patterns))}")
lines.append("")
lines.append(f"Total AGENTS.md files: {len(distributed_result.placements)}")
return "\n".join(lines)
def _generate_distributed_summary(self, distributed_result, config: CompilationConfig) -> str:
"""Generate a summary of distributed compilation results.
Args:
distributed_result: Result from distributed compilation.
config (CompilationConfig): Compilation configuration.
Returns:
str: Summary content.
"""
lines = [
"# Distributed AGENTS.md Compilation Summary",
"",
f"Generated {len(distributed_result.placements)} AGENTS.md files:",
""
]
for placement in distributed_result.placements:
try:
rel_path = placement.agents_path.relative_to(self.base_dir.resolve())
except ValueError:
rel_path = placement.agents_path
lines.append(f"- {rel_path} ({len(placement.instructions)} instructions)")
lines.extend([
"",
f"Total instructions: {distributed_result.stats.get('total_instructions_placed', 0)}",
f"Total patterns: {distributed_result.stats.get('total_patterns_covered', 0)}",
"",
"Use 'apm compile --single-agents' for traditional single-file compilation."
])
return "\n".join(lines)
def compile_agents_md(
primitives: Optional[PrimitiveCollection] = None,
output_path: str = "AGENTS.md",
chatmode: Optional[str] = None,
dry_run: bool = False,
base_dir: str = "."
) -> str:
"""Generate AGENTS.md with conditional sections.
Args:
primitives (Optional[PrimitiveCollection]): Primitives to use, or None to discover.
output_path (str): Output file path. Defaults to "AGENTS.md".
chatmode (str): Specific chatmode to use, or None for default.
dry_run (bool): If True, don't write output file. Defaults to False.
base_dir (str): Base directory for compilation. Defaults to current directory.
Returns:
str: Generated AGENTS.md content.
"""
# Create configuration - use single-file mode for backward compatibility
config = CompilationConfig(
output_path=output_path,
chatmode=chatmode,
dry_run=dry_run,
strategy="single-file" # Force single-file mode for backward compatibility
)
# Create compiler and compile
compiler = AgentsCompiler(base_dir)
result = compiler.compile(config, primitives)
if not result.success:
raise RuntimeError(f"Compilation failed: {'; '.join(result.errors)}")
return result.content

View File

@@ -0,0 +1,18 @@
"""Shared constants for compilation extensions (constitution injection, etc.).
Also contains shared markers for build metadata stabilization. We intentionally
avoid timestamps in generated artifacts to guarantee byte-level idempotency; a
deterministic Build ID (content hash) is substituted post-generation.
"""
# Constitution injection markers
CONSTITUTION_MARKER_BEGIN = "<!-- SPEC-KIT CONSTITUTION: BEGIN -->"
CONSTITUTION_MARKER_END = "<!-- SPEC-KIT CONSTITUTION: END -->"
CONSTITUTION_RELATIVE_PATH = ".specify/memory/constitution.md" # repo-root relative
# Build ID placeholder & regex pattern (line-level). The placeholder line is
# inserted during initial template generation; after all transformations
# (constitution injection, link resolution, etc.) we compute a SHA256 of the
# final content with this line removed and then replace it with the truncated
# hash. This ensures the hash is not self-referential and remains stable.
BUILD_ID_PLACEHOLDER = "<!-- Build ID: __BUILD_ID__ -->"

View File

@@ -0,0 +1,33 @@
"""Utilities for reading Spec Kit style constitution file."""
from __future__ import annotations
from pathlib import Path
from typing import Optional
from .constants import CONSTITUTION_RELATIVE_PATH
def find_constitution(base_dir: Path) -> Path:
"""Return path to constitution.md if present, else Path that does not exist.
We keep logic trivial for Phase 0: fixed location under memory/.
Later phases may support multiple shards / namespacing.
"""
return base_dir / CONSTITUTION_RELATIVE_PATH
def read_constitution(base_dir: Path) -> Optional[str]:
"""Read full constitution content if file exists.
Args:
base_dir: Repository root path.
Returns:
Full file text or None if absent.
"""
path = find_constitution(base_dir)
if not path.exists() or not path.is_file():
return None
try:
return path.read_text(encoding="utf-8")
except OSError:
return None

View File

@@ -0,0 +1,96 @@
"""Rendering & parsing of injected constitution block in AGENTS.md."""
from __future__ import annotations
import hashlib
import re
from dataclasses import dataclass
from typing import Optional
from .constants import (
CONSTITUTION_MARKER_BEGIN,
CONSTITUTION_MARKER_END,
CONSTITUTION_RELATIVE_PATH,
)
HASH_PREFIX = "hash:"
def compute_constitution_hash(content: str) -> str:
"""Compute stable truncated SHA256 hash of full constitution content."""
sha = hashlib.sha256(content.encode("utf-8"))
return sha.hexdigest()[:12]
def render_block(constitution_content: str) -> str:
"""Render full constitution block with markers and hash line.
The block mirrors spec requirement: entire file as-is within markers.
"""
h = compute_constitution_hash(constitution_content)
header_meta = f"{HASH_PREFIX} {h} path: {CONSTITUTION_RELATIVE_PATH}"
# Ensure trailing newline for clean separation from compiled content
body = constitution_content.rstrip() + "\n"
return (
f"{CONSTITUTION_MARKER_BEGIN}\n"
f"{header_meta}\n"
f"{body}"
f"{CONSTITUTION_MARKER_END}\n"
"\n" # blank line after block
)
@dataclass
class ExistingBlock:
raw: str
hash: Optional[str]
start_index: int
end_index: int
BLOCK_REGEX = re.compile(
rf"({re.escape(CONSTITUTION_MARKER_BEGIN)})(.*?)({re.escape(CONSTITUTION_MARKER_END)})",
re.DOTALL,
)
HASH_LINE_REGEX = re.compile(r"hash:\s*([0-9a-fA-F]{6,64})")
def find_existing_block(content: str) -> Optional[ExistingBlock]:
"""Locate existing constitution block and extract its hash if present."""
match = BLOCK_REGEX.search(content)
if not match:
return None
block_text = match.group(0)
hash_match = HASH_LINE_REGEX.search(block_text)
h = hash_match.group(1) if hash_match else None
return ExistingBlock(raw=block_text, hash=h, start_index=match.start(), end_index=match.end())
def inject_or_update(existing_agents: str, new_block: str, place_top: bool = True) -> tuple[str, str]:
"""Insert or update constitution block in existing AGENTS.md content.
Args:
existing_agents: Current AGENTS.md text (may be empty).
new_block: Rendered constitution block (already ends with newline).
place_top: Always True for Phase 0 (prepend at top).
Returns:
(updated_text, status) where status in CREATED|UPDATED|UNCHANGED.
"""
existing_block = find_existing_block(existing_agents)
if existing_block:
if existing_block.raw == new_block.rstrip(): # exclude trailing blank block newline
return existing_agents, "UNCHANGED"
# Replace existing block span with new block
updated = existing_agents[: existing_block.start_index] + new_block.rstrip() + existing_agents[existing_block.end_index :]
# Ensure trailing newline after block + rest
if not updated.startswith(new_block):
# If markers were not at top previously and we want top placement, move them
if place_top:
body_without_block = updated.replace(new_block.rstrip(), "").lstrip("\n")
updated = new_block + body_without_block
return updated, "UPDATED"
# No existing block
if place_top:
return new_block + existing_agents.lstrip("\n"), "CREATED"
return existing_agents + ("\n" if not existing_agents.endswith("\n") else "") + new_block, "CREATED"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,685 @@
"""Distributed AGENTS.md compilation system following the Minimal Context Principle.
This module implements hierarchical directory-based distribution to generate multiple
AGENTS.md files across a project's directory structure, following the AGENTS.md standard
for nested agent context files.
"""
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
from collections import defaultdict
from ..primitives.models import Instruction, PrimitiveCollection
from ..version import get_version
from .template_builder import TemplateData, find_chatmode_by_name
from .constants import BUILD_ID_PLACEHOLDER
from .context_optimizer import ContextOptimizer
from ..output.formatters import CompilationFormatter
from ..output.models import CompilationResults
@dataclass
class DirectoryMap:
"""Mapping of directory structure analysis."""
directories: Dict[Path, Set[str]] # directory -> set of applicable file patterns
depth_map: Dict[Path, int] # directory -> depth level
parent_map: Dict[Path, Optional[Path]] # directory -> parent directory
def get_max_depth(self) -> int:
"""Get maximum depth in the directory structure."""
return max(self.depth_map.values()) if self.depth_map else 0
@dataclass
class PlacementResult:
"""Result of AGENTS.md placement analysis."""
agents_path: Path
instructions: List[Instruction]
inherited_instructions: List[Instruction] = field(default_factory=list)
coverage_patterns: Set[str] = field(default_factory=set)
source_attribution: Dict[str, str] = field(default_factory=dict) # instruction_id -> source
@dataclass
class CompilationResult:
"""Result of distributed AGENTS.md compilation."""
success: bool
placements: List[PlacementResult]
content_map: Dict[Path, str] # agents_path -> content
warnings: List[str] = field(default_factory=list)
errors: List[str] = field(default_factory=list)
stats: Dict[str, float] = field(default_factory=dict) # Support optimization metrics
class DistributedAgentsCompiler:
"""Main compiler for generating distributed AGENTS.md files."""
def __init__(self, base_dir: str = "."):
"""Initialize the distributed AGENTS.md compiler.
Args:
base_dir (str): Base directory for compilation.
"""
try:
self.base_dir = Path(base_dir).resolve()
except (OSError, FileNotFoundError):
self.base_dir = Path(base_dir).absolute()
self.warnings: List[str] = []
self.errors: List[str] = []
self.total_files_written = 0
self.context_optimizer = ContextOptimizer(str(self.base_dir))
self.output_formatter = CompilationFormatter()
self._placement_map = None
def compile_distributed(
self,
primitives: PrimitiveCollection,
config: Optional[dict] = None
) -> CompilationResult:
"""Compile primitives into distributed AGENTS.md files.
Args:
primitives (PrimitiveCollection): Collection of primitives to compile.
config (Optional[dict]): Configuration for distributed compilation.
- clean_orphaned (bool): Remove orphaned AGENTS.md files. Default: False
- dry_run (bool): Preview mode, don't write files. Default: False
Returns:
CompilationResult: Result of the distributed compilation.
"""
self.warnings.clear()
self.errors.clear()
try:
# Configuration with defaults aligned to Minimal Context Principle
config = config or {}
min_instructions = config.get('min_instructions_per_file', 1) # Default to 1 for minimal context
source_attribution = config.get('source_attribution', True)
debug = config.get('debug', False)
clean_orphaned = config.get('clean_orphaned', False)
dry_run = config.get('dry_run', False)
# Phase 1: Directory structure analysis
directory_map = self.analyze_directory_structure(primitives.instructions)
# Phase 2: Determine optimal AGENTS.md placement
placement_map = self.determine_agents_placement(
primitives.instructions,
directory_map,
min_instructions=min_instructions,
debug=debug
)
# Phase 3: Generate distributed AGENTS.md files
placements = self.generate_distributed_agents_files(
placement_map,
primitives,
source_attribution=source_attribution
)
# Phase 4: Handle orphaned file cleanup
generated_paths = [p.agents_path for p in placements]
orphaned_files = self._find_orphaned_agents_files(generated_paths)
if orphaned_files:
# Always show warnings about orphaned files
warning_messages = self._generate_orphan_warnings(orphaned_files)
if warning_messages:
self.warnings.extend(warning_messages)
# Only perform actual cleanup if not dry_run and clean_orphaned is True
if not dry_run and clean_orphaned:
cleanup_messages = self._cleanup_orphaned_files(orphaned_files, dry_run=False)
if cleanup_messages:
self.warnings.extend(cleanup_messages)
# Phase 5: Validate coverage
coverage_validation = self._validate_coverage(placements, primitives.instructions)
if coverage_validation:
self.warnings.extend(coverage_validation)
# Compile statistics
stats = self._compile_distributed_stats(placements, primitives)
return CompilationResult(
success=len(self.errors) == 0,
placements=placements,
content_map={p.agents_path: self._generate_agents_content(p, primitives) for p in placements},
warnings=self.warnings.copy(),
errors=self.errors.copy(),
stats=stats
)
except Exception as e:
self.errors.append(f"Distributed compilation failed: {str(e)}")
return CompilationResult(
success=False,
placements=[],
content_map={},
warnings=self.warnings.copy(),
errors=self.errors.copy(),
stats={}
)
def analyze_directory_structure(self, instructions: List[Instruction]) -> DirectoryMap:
"""Analyze project directory structure based on instruction patterns.
Args:
instructions (List[Instruction]): List of instructions to analyze.
Returns:
DirectoryMap: Analysis of the directory structure.
"""
directories: Dict[Path, Set[str]] = defaultdict(set)
depth_map: Dict[Path, int] = {}
parent_map: Dict[Path, Optional[Path]] = {}
# Analyze each instruction's applyTo pattern
for instruction in instructions:
if not instruction.apply_to:
continue
pattern = instruction.apply_to
# Extract directory paths from pattern
dirs = self._extract_directories_from_pattern(pattern)
for dir_path in dirs:
abs_dir = self.base_dir / dir_path
directories[abs_dir].add(pattern)
# Calculate depth and parent relationships
depth = len(abs_dir.relative_to(self.base_dir).parts)
depth_map[abs_dir] = depth
if depth > 0:
parent_dir = abs_dir.parent
parent_map[abs_dir] = parent_dir
# Ensure parent is also tracked
if parent_dir not in directories:
directories[parent_dir] = set()
else:
parent_map[abs_dir] = None
# Add base directory
directories[self.base_dir].update(instruction.apply_to for instruction in instructions if instruction.apply_to)
depth_map[self.base_dir] = 0
parent_map[self.base_dir] = None
return DirectoryMap(
directories=dict(directories),
depth_map=depth_map,
parent_map=parent_map
)
def determine_agents_placement(
self,
instructions: List[Instruction],
directory_map: DirectoryMap,
min_instructions: int = 1,
debug: bool = False
) -> Dict[Path, List[Instruction]]:
"""Determine optimal AGENTS.md file placement using Context Optimization Engine.
Following the Minimal Context Principle and Context Optimization, creates
focused AGENTS.md files that minimize context pollution while maximizing
relevance for agents working in specific directories.
Args:
instructions (List[Instruction]): List of instructions to place.
directory_map (DirectoryMap): Directory structure analysis.
min_instructions (int): Minimum instructions (default 1 for minimal context).
max_depth (int): Maximum depth for placement.
Returns:
Dict[Path, List[Instruction]]: Optimized mapping of directory paths to instructions.
"""
# Use the Context Optimization Engine for intelligent placement
optimized_placement = self.context_optimizer.optimize_instruction_placement(
instructions,
verbose=debug,
enable_timing=debug # Enable timing when debug mode is on
)
# Special case: if no instructions but constitution exists, create root placement
if not optimized_placement:
from .constitution import find_constitution
constitution_path = find_constitution(Path(self.base_dir))
if constitution_path.exists():
# Create an empty placement for the root directory to enable verbose output
optimized_placement = {Path(self.base_dir): []}
# Store optimization results for output formatting later
# Update with proper dry run status in the final result
self._placement_map = optimized_placement
# Remove the verbose warning log - we'll show this in professional output instead
# Filter out directories with too few instructions if specified
if min_instructions > 1:
filtered_placement = {}
for dir_path, dir_instructions in optimized_placement.items():
if len(dir_instructions) >= min_instructions or dir_path == self.base_dir:
filtered_placement[dir_path] = dir_instructions
else:
# Move instructions to parent directory
parent_dir = dir_path.parent if dir_path != self.base_dir else self.base_dir
if parent_dir not in filtered_placement:
filtered_placement[parent_dir] = []
filtered_placement[parent_dir].extend(dir_instructions)
return filtered_placement
return optimized_placement
def generate_distributed_agents_files(
self,
placement_map: Dict[Path, List[Instruction]],
primitives: PrimitiveCollection,
source_attribution: bool = True
) -> List[PlacementResult]:
"""Generate distributed AGENTS.md file contents.
Args:
placement_map (Dict[Path, List[Instruction]]): Directory to instructions mapping.
primitives (PrimitiveCollection): Full primitive collection.
source_attribution (bool): Whether to include source attribution.
Returns:
List[PlacementResult]: List of placement results with content.
"""
placements = []
# Special case: if no instructions but constitution exists, create root placement
if not placement_map:
from .constitution import find_constitution
constitution_path = find_constitution(Path(self.base_dir))
if constitution_path.exists():
# Create a root placement for constitution-only projects
root_path = Path(self.base_dir)
agents_path = root_path / "AGENTS.md"
placement = PlacementResult(
agents_path=agents_path,
instructions=[], # No instructions, just constitution
coverage_patterns=set(), # No patterns since no instructions
source_attribution={"constitution": "constitution.md"} if source_attribution else {}
)
placements.append(placement)
else:
# Normal case: create placements for each entry in placement_map
for dir_path, instructions in placement_map.items():
agents_path = dir_path / "AGENTS.md"
# Build source attribution map if enabled
source_map = {}
if source_attribution:
for instruction in instructions:
source_info = getattr(instruction, 'source', 'local')
source_map[str(instruction.file_path)] = source_info
# Extract coverage patterns
patterns = set()
for instruction in instructions:
if instruction.apply_to:
patterns.add(instruction.apply_to)
placement = PlacementResult(
agents_path=agents_path,
instructions=instructions,
coverage_patterns=patterns,
source_attribution=source_map
)
placements.append(placement)
return placements
def get_compilation_results_for_display(self, is_dry_run: bool = False) -> Optional[CompilationResults]:
"""Get compilation results for CLI display integration.
Args:
is_dry_run: Whether this is a dry run.
Returns:
CompilationResults if available, None otherwise.
"""
if self._placement_map:
# Generate fresh compilation results with correct dry run status
compilation_results = self.context_optimizer.get_compilation_results(
self._placement_map,
is_dry_run=is_dry_run
)
# Merge distributed compiler's warnings (like orphan warnings) with optimizer warnings
all_warnings = compilation_results.warnings + self.warnings
# Create new compilation results with merged warnings
from ..output.models import CompilationResults
return CompilationResults(
project_analysis=compilation_results.project_analysis,
optimization_decisions=compilation_results.optimization_decisions,
placement_summaries=compilation_results.placement_summaries,
optimization_stats=compilation_results.optimization_stats,
warnings=all_warnings,
errors=compilation_results.errors + self.errors,
is_dry_run=is_dry_run
)
return None
def _extract_directories_from_pattern(self, pattern: str) -> List[Path]:
"""Extract potential directory paths from a file pattern.
Args:
pattern (str): File pattern like "src/**/*.py" or "docs/*.md"
Returns:
List[Path]: List of directory paths that could contain matching files.
"""
directories = []
# Remove filename part and wildcards to get directory structure
# Examples:
# "src/**/*.py" -> ["src"]
# "docs/*.md" -> ["docs"]
# "**/*.py" -> ["."] (current directory)
# "*.py" -> ["."] (current directory)
if pattern.startswith("**/"):
# Global pattern - applies to all directories
directories.append(Path("."))
elif "/" in pattern:
# Extract directory part
dir_part = pattern.split("/")[0]
if not dir_part.startswith("*"):
directories.append(Path(dir_part))
else:
directories.append(Path("."))
else:
# No directory part - applies to current directory
directories.append(Path("."))
return directories
def _find_best_directory(
self,
instruction: Instruction,
directory_map: DirectoryMap,
max_depth: int
) -> Path:
"""Find the best directory for placing an instruction.
Args:
instruction (Instruction): Instruction to place.
directory_map (DirectoryMap): Directory structure analysis.
max_depth (int): Maximum allowed depth.
Returns:
Path: Best directory path for the instruction.
"""
if not instruction.apply_to:
return self.base_dir
pattern = instruction.apply_to
best_dir = self.base_dir
best_specificity = 0
for dir_path in directory_map.directories:
# Skip directories that are too deep
if directory_map.depth_map.get(dir_path, 0) > max_depth:
continue
# Check if this directory could contain files matching the pattern
if pattern in directory_map.directories[dir_path]:
# Prefer more specific (deeper) directories
specificity = directory_map.depth_map.get(dir_path, 0)
if specificity > best_specificity:
best_specificity = specificity
best_dir = dir_path
return best_dir
def _generate_agents_content(
self,
placement: PlacementResult,
primitives: PrimitiveCollection
) -> str:
"""Generate AGENTS.md content for a specific placement.
Args:
placement (PlacementResult): Placement result with instructions.
primitives (PrimitiveCollection): Full primitive collection.
Returns:
str: Generated AGENTS.md content.
"""
sections = []
# Header with source attribution
sections.append("# AGENTS.md")
sections.append("<!-- Generated by APM CLI from distributed .apm/ primitives -->")
sections.append(BUILD_ID_PLACEHOLDER)
sections.append(f"<!-- APM Version: {get_version()} -->")
# Add source attribution summary if enabled
if placement.source_attribution:
sources = set(placement.source_attribution.values())
if len(sources) > 1:
sections.append(f"<!-- Sources: {', '.join(sorted(sources))} -->")
else:
sections.append(f"<!-- Source: {list(sources)[0] if sources else 'local'} -->")
sections.append("")
# Group instructions by pattern
pattern_groups: Dict[str, List[Instruction]] = defaultdict(list)
for instruction in placement.instructions:
if instruction.apply_to:
pattern_groups[instruction.apply_to].append(instruction)
# Generate sections for each pattern
for pattern, pattern_instructions in sorted(pattern_groups.items()):
sections.append(f"## Files matching `{pattern}`")
sections.append("")
for instruction in pattern_instructions:
content = instruction.content.strip()
if content:
# Add source attribution for individual instructions
if placement.source_attribution:
source = placement.source_attribution.get(str(instruction.file_path), 'local')
try:
rel_path = instruction.file_path.relative_to(self.base_dir)
except ValueError:
rel_path = instruction.file_path
sections.append(f"<!-- Source: {source} {rel_path} -->")
sections.append(content)
sections.append("")
# Footer
sections.append("---")
sections.append("*This file was generated by APM CLI. Do not edit manually.*")
sections.append("*To regenerate: `specify apm compile`*")
sections.append("")
return "\n".join(sections)
def _validate_coverage(
self,
placements: List[PlacementResult],
all_instructions: List[Instruction]
) -> List[str]:
"""Validate that all instructions are covered by placements.
Args:
placements (List[PlacementResult]): Generated placements.
all_instructions (List[Instruction]): All available instructions.
Returns:
List[str]: List of coverage warnings.
"""
warnings = []
placed_instructions = set()
for placement in placements:
placed_instructions.update(str(inst.file_path) for inst in placement.instructions)
all_instruction_paths = set(str(inst.file_path) for inst in all_instructions)
missing_instructions = all_instruction_paths - placed_instructions
if missing_instructions:
warnings.append(f"Instructions not placed in any AGENTS.md: {', '.join(missing_instructions)}")
return warnings
def _find_orphaned_agents_files(self, generated_paths: List[Path]) -> List[Path]:
"""Find existing AGENTS.md files that weren't generated in the current compilation.
Args:
generated_paths (List[Path]): List of AGENTS.md files generated in current run.
Returns:
List[Path]: List of orphaned AGENTS.md files that should be cleaned up.
"""
orphaned_files = []
generated_set = set(generated_paths)
# Find all existing AGENTS.md files in the project
for agents_file in self.base_dir.rglob("AGENTS.md"):
# Skip files that are outside our project or in special directories
try:
relative_path = agents_file.relative_to(self.base_dir)
# Skip files in certain directories that shouldn't be cleaned
skip_dirs = {".git", ".apm", "node_modules", "__pycache__", ".pytest_cache", "apm_modules"}
if any(part in skip_dirs for part in relative_path.parts):
continue
# If this existing file wasn't generated in current run, it's orphaned
if agents_file not in generated_set:
orphaned_files.append(agents_file)
except ValueError:
# File is outside base_dir, skip it
continue
return orphaned_files
def _generate_orphan_warnings(self, orphaned_files: List[Path]) -> List[str]:
"""Generate warning messages for orphaned AGENTS.md files.
Args:
orphaned_files (List[Path]): List of orphaned files to warn about.
Returns:
List[str]: List of warning messages.
"""
warning_messages = []
if not orphaned_files:
return warning_messages
# Professional warning format with readable list for multiple files
if len(orphaned_files) == 1:
rel_path = orphaned_files[0].relative_to(self.base_dir)
warning_messages.append(f"Orphaned AGENTS.md found: {rel_path} - run 'apm compile --clean' to remove")
else:
# For multiple files, create a single multi-line warning message
file_list = []
for file_path in orphaned_files[:5]: # Show first 5
rel_path = file_path.relative_to(self.base_dir)
file_list.append(f"{rel_path}")
if len(orphaned_files) > 5:
file_list.append(f" • ...and {len(orphaned_files) - 5} more")
# Create one cohesive warning message
files_text = "\n".join(file_list)
warning_messages.append(f"Found {len(orphaned_files)} orphaned AGENTS.md files:\n{files_text}\n Run 'apm compile --clean' to remove orphaned files")
return warning_messages
def _cleanup_orphaned_files(self, orphaned_files: List[Path], dry_run: bool = False) -> List[str]:
"""Actually remove orphaned AGENTS.md files.
Args:
orphaned_files (List[Path]): List of orphaned files to remove.
dry_run (bool): If True, don't actually remove files, just report what would be removed.
Returns:
List[str]: List of cleanup status messages.
"""
cleanup_messages = []
if not orphaned_files:
return cleanup_messages
if dry_run:
# In dry-run mode, just report what would be cleaned
cleanup_messages.append(f"🧹 Would clean up {len(orphaned_files)} orphaned AGENTS.md files")
for file_path in orphaned_files:
rel_path = file_path.relative_to(self.base_dir)
cleanup_messages.append(f"{rel_path}")
else:
# Actually perform the cleanup
cleanup_messages.append(f"🧹 Cleaning up {len(orphaned_files)} orphaned AGENTS.md files")
for file_path in orphaned_files:
try:
rel_path = file_path.relative_to(self.base_dir)
file_path.unlink()
cleanup_messages.append(f" ✓ Removed {rel_path}")
except Exception as e:
cleanup_messages.append(f" ✗ Failed to remove {rel_path}: {str(e)}")
return cleanup_messages
def _compile_distributed_stats(
self,
placements: List[PlacementResult],
primitives: PrimitiveCollection
) -> Dict[str, float]:
"""Compile statistics about the distributed compilation with optimization metrics.
Args:
placements (List[PlacementResult]): Generated placements.
primitives (PrimitiveCollection): Full primitive collection.
Returns:
Dict[str, float]: Compilation statistics including optimization metrics.
"""
total_instructions = sum(len(p.instructions) for p in placements)
total_patterns = sum(len(p.coverage_patterns) for p in placements)
# Get optimization metrics
placement_map = {Path(p.agents_path.parent): p.instructions for p in placements}
optimization_stats = self.context_optimizer.get_optimization_stats(placement_map)
# Combine traditional stats with optimization metrics
stats = {
"agents_files_generated": len(placements),
"total_instructions_placed": total_instructions,
"total_patterns_covered": total_patterns,
"primitives_found": primitives.count(),
"chatmodes": len(primitives.chatmodes),
"instructions": len(primitives.instructions),
"contexts": len(primitives.contexts)
}
# Add optimization metrics from OptimizationStats object
if optimization_stats:
stats.update({
"average_context_efficiency": optimization_stats.average_context_efficiency,
"pollution_improvement": optimization_stats.pollution_improvement,
"baseline_efficiency": optimization_stats.baseline_efficiency,
"placement_accuracy": optimization_stats.placement_accuracy,
"generation_time_ms": optimization_stats.generation_time_ms,
"total_agents_files": optimization_stats.total_agents_files,
"directories_analyzed": optimization_stats.directories_analyzed
})
return stats

View File

@@ -0,0 +1,91 @@
"""High-level constitution injection workflow used by compile command."""
from __future__ import annotations
from pathlib import Path
from typing import Optional, Literal
from .constitution import read_constitution
from .constitution_block import render_block, find_existing_block
from .constants import CONSTITUTION_MARKER_BEGIN, CONSTITUTION_MARKER_END
InjectionStatus = Literal["CREATED", "UPDATED", "UNCHANGED", "SKIPPED", "MISSING"]
class ConstitutionInjector:
"""Encapsulates constitution detection + injection logic."""
def __init__(self, base_dir: str):
self.base_dir = Path(base_dir)
def inject(self, compiled_content: str, with_constitution: bool, output_path: Path) -> tuple[str, InjectionStatus, Optional[str]]:
"""Return final AGENTS.md content after optional injection.
Args:
compiled_content: Newly compiled content (without constitution block).
with_constitution: Whether to perform injection (True) or preserve existing block (False).
output_path: Existing AGENTS.md path (may not exist) for preservation logic.
Returns:
(final_content, status, hash_or_none)
"""
existing_content = ""
if output_path.exists():
try:
existing_content = output_path.read_text(encoding="utf-8")
except OSError:
existing_content = ""
# Helper to split header/body from freshly compiled content.
def _split_header(content: str) -> tuple[str, str]:
# Header ends at the first double newline (blank line separating header from body)
marker = "\n\n"
if marker in content:
idx = content.index(marker)
return content[: idx + len(marker)], content[idx + len(marker) :]
# Fallback: treat whole content as header
return content, ""
header_part, body_part = _split_header(compiled_content)
if not with_constitution:
# If skipping, we preserve existing block if present but enforce ordering: header first, block (if any), then body.
existing_block = find_existing_block(existing_content)
if existing_block:
final = header_part + existing_block.raw.rstrip() + "\n\n" + body_part.lstrip("\n")
return final, "SKIPPED", None
return compiled_content, "SKIPPED", None
constitution_text = read_constitution(self.base_dir)
if constitution_text is None:
existing_block = find_existing_block(existing_content)
if existing_block:
final = header_part + existing_block.raw.rstrip() + "\n\n" + body_part.lstrip("\n")
return final, "MISSING", None
return compiled_content, "MISSING", None
new_block = render_block(constitution_text)
existing_block = find_existing_block(existing_content)
if existing_block:
# Compare raw block bodies (strip trailing newlines for stable compare)
if existing_block.raw.rstrip() == new_block.rstrip():
status = "UNCHANGED"
block_to_use = existing_block.raw.rstrip()
else:
status = "UPDATED"
block_to_use = new_block.rstrip()
else:
status = "CREATED"
block_to_use = new_block.rstrip()
hash_line = new_block.splitlines()[1] if len(new_block.splitlines()) > 1 else ""
hash_value = None
if hash_line.startswith("hash:"):
parts = hash_line.split()
if len(parts) >= 2:
hash_value = parts[1]
final_content = header_part + block_to_use + "\n\n" + body_part.lstrip("\n")
# Ensure single trailing newline
if not final_content.endswith("\n"):
final_content += "\n"
return final_content, status, hash_value

View File

@@ -0,0 +1,181 @@
"""Markdown link resolution for AGENTS.md compilation."""
import re
from pathlib import Path
from typing import List, Dict, Tuple, Optional
def resolve_markdown_links(content: str, base_path: Path) -> str:
"""Resolve markdown links and inline referenced content.
Args:
content (str): Content with markdown links to resolve.
base_path (Path): Base directory for resolving relative paths.
Returns:
str: Content with resolved links and inlined content where appropriate.
"""
# Pattern to match markdown links: [text](path)
link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
def replace_link(match):
text = match.group(1)
path = match.group(2)
# Skip external URLs
if path.startswith(('http://', 'https://', 'ftp://', 'mailto:')):
return match.group(0) # Return original link
# Skip anchors
if path.startswith('#'):
return match.group(0) # Return original link
# Resolve relative path
full_path = _resolve_path(path, base_path)
if full_path and full_path.exists() and full_path.is_file():
# For certain file types, inline the content
if full_path.suffix.lower() in ['.md', '.txt']:
try:
file_content = full_path.read_text(encoding='utf-8')
# Remove frontmatter if present
file_content = _remove_frontmatter(file_content)
return f"**{text}**:\n\n{file_content}"
except (OSError, UnicodeDecodeError):
# Fall back to original link if file can't be read
return match.group(0)
else:
# For other file types, keep the link but update path if needed
return match.group(0)
else:
# File doesn't exist, keep original link (will be caught by validation)
return match.group(0)
return re.sub(link_pattern, replace_link, content)
def validate_link_targets(content: str, base_path: Path) -> List[str]:
"""Validate that all referenced files exist.
Args:
content (str): Content to validate links in.
base_path (Path): Base directory for resolving relative paths.
Returns:
List[str]: List of error messages for missing or invalid links.
"""
errors = []
# Check markdown links
link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
for match in re.finditer(link_pattern, content):
text = match.group(1)
path = match.group(2)
# Skip external URLs and anchors
if (path.startswith(('http://', 'https://', 'ftp://', 'mailto:')) or
path.startswith('#')):
continue
# Resolve and check path
full_path = _resolve_path(path, base_path)
if not full_path or not full_path.exists():
errors.append(f"Referenced file not found: {path} (in link '{text}')")
elif not full_path.is_file() and not full_path.is_dir():
errors.append(f"Referenced path is neither a file nor directory: {path} (in link '{text}')")
return errors
def _resolve_path(path: str, base_path: Path) -> Optional[Path]:
"""Resolve a relative path against a base path.
Args:
path (str): Relative path to resolve.
base_path (Path): Base directory for resolution.
Returns:
Optional[Path]: Resolved path or None if invalid.
"""
try:
if Path(path).is_absolute():
return Path(path)
else:
return base_path / path
except (OSError, ValueError):
return None
def _remove_frontmatter(content: str) -> str:
"""Remove YAML frontmatter from content.
Args:
content (str): Content that may contain frontmatter.
Returns:
str: Content without frontmatter.
"""
# Remove YAML frontmatter (--- at start, --- at end)
if content.startswith('---\n'):
lines = content.split('\n')
in_frontmatter = True
content_lines = []
for i, line in enumerate(lines[1:], 1): # Skip first ---
if line.strip() == '---' and in_frontmatter:
in_frontmatter = False
continue
if not in_frontmatter:
content_lines.append(line)
content = '\n'.join(content_lines)
return content.strip()
def _detect_circular_references(content: str, base_path: Path, visited: Optional[set] = None) -> List[str]:
"""Detect circular references in markdown links.
Args:
content (str): Content to check for circular references.
base_path (Path): Base directory for resolving paths.
visited (Optional[set]): Set of already visited files.
Returns:
List[str]: List of circular reference errors.
"""
if visited is None:
visited = set()
errors = []
current_file = base_path
if current_file in visited:
errors.append(f"Circular reference detected: {current_file}")
return errors
visited.add(current_file)
# Check markdown links for potential circular references
link_pattern = r'\[([^\]]+)\]\(([^)]+)\)'
for match in re.finditer(link_pattern, content):
path = match.group(2)
# Skip external URLs and anchors
if (path.startswith(('http://', 'https://', 'ftp://', 'mailto:')) or
path.startswith('#')):
continue
full_path = _resolve_path(path, base_path.parent if base_path.is_file() else base_path)
if full_path and full_path.exists() and full_path.is_file():
if full_path.suffix.lower() in ['.md', '.txt']:
try:
linked_content = full_path.read_text(encoding='utf-8')
errors.extend(_detect_circular_references(linked_content, full_path, visited.copy()))
except (OSError, UnicodeDecodeError):
continue
return errors

Some files were not shown because too many files have changed in this diff Show More