mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 04:45:43 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e3ba4eb53 |
@@ -48,6 +48,8 @@
|
||||
"openai.chatgpt",
|
||||
// Kilo Code
|
||||
"kilocode.Kilo-Code",
|
||||
// Roo Code
|
||||
"RooVeterinaryInc.roo-cline",
|
||||
// Claude Code
|
||||
"anthropic.claude-code"
|
||||
],
|
||||
|
||||
@@ -56,7 +56,7 @@ run_command "npm install -g @jetbrains/junie-cli@latest"
|
||||
echo "✅ Done"
|
||||
|
||||
echo -e "\n🤖 Installing Pi Coding Agent..."
|
||||
run_command "npm install -g @earendil-works/pi-coding-agent@latest"
|
||||
run_command "npm install -g @mariozechner/pi-coding-agent@latest"
|
||||
echo "✅ Done"
|
||||
|
||||
echo -e "\n🤖 Installing Kiro CLI..."
|
||||
@@ -88,9 +88,9 @@ fi
|
||||
run_command "$kiro_binary --help > /dev/null"
|
||||
echo "✅ Done"
|
||||
|
||||
echo -e "\n🤖 Installing Kimi Code CLI..."
|
||||
echo -e "\n🤖 Installing Kimi CLI..."
|
||||
# https://code.kimi.com
|
||||
run_command "npm install -g @moonshot-ai/kimi-code@latest"
|
||||
run_command "pipx install kimi-cli"
|
||||
echo "✅ Done"
|
||||
|
||||
echo -e "\n🤖 Installing CodeBuddy CLI..."
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
2
.github/ISSUE_TEMPLATE/agent_request.yml
vendored
@@ -8,7 +8,7 @@ body:
|
||||
value: |
|
||||
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
|
||||
|
||||
**Currently supported agents**: Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Cursor, Devin for Terminal, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Hermes Agent, IBM Bob, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, Oh My Pi, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, ZCode, Zed
|
||||
**Currently supported agents**: Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Cursor, Devin for Terminal, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Hermes Agent, IBM Bob, iFlow CLI, Junie, Kilo Code, Kimi Code, Kiro CLI, Lingma, Mistral Vibe, opencode, Pi Coding Agent, Qoder CLI, Qwen Code, Roo Code, RovoDev ACLI, SHAI, Tabnine CLI, Trae, Windsurf, ZCode, Zed
|
||||
|
||||
- type: input
|
||||
id: agent-name
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -78,21 +78,23 @@ body:
|
||||
- Goose
|
||||
- Hermes Agent
|
||||
- IBM Bob
|
||||
- iFlow CLI
|
||||
- Junie
|
||||
- Kilo Code
|
||||
- Kimi Code
|
||||
- Kiro CLI
|
||||
- Lingma
|
||||
- Mistral Vibe
|
||||
- Oh My Pi
|
||||
- opencode
|
||||
- Pi Coding Agent
|
||||
- Qoder CLI
|
||||
- Qwen Code
|
||||
- Roo Code
|
||||
- RovoDev ACLI
|
||||
- SHAI
|
||||
- Tabnine CLI
|
||||
- Trae
|
||||
- Windsurf
|
||||
- ZCode
|
||||
- Zed
|
||||
- Not applicable
|
||||
|
||||
293
.github/ISSUE_TEMPLATE/bundle_submission.yml
vendored
293
.github/ISSUE_TEMPLATE/bundle_submission.yml
vendored
@@ -1,293 +0,0 @@
|
||||
name: Bundle Submission
|
||||
description: Submit your bundle metadata for community catalog validation
|
||||
title: "[Bundle]: Add "
|
||||
labels: ["enhancement", "needs-triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for contributing a bundle! This template captures metadata for maintainers to validate formatting, links, component resolution, and installation evidence. Maintainers do not audit, endorse, or support bundle code or installed components.
|
||||
|
||||
**Before submitting:**
|
||||
- Review the [Bundles reference](https://github.com/github/spec-kit/blob/main/docs/reference/bundles.md)
|
||||
- Ensure your bundle has a valid `bundle.yml` manifest
|
||||
- Create a GitHub release with a versioned bundle artifact
|
||||
- Test installation from a downloaded artifact: `specify bundle install ./your-bundle-1.0.0.zip`
|
||||
- If you host a bundle catalog, test catalog installation with `specify bundle catalog add <catalog-url> --id <catalog-id> --policy install-allowed` and `specify bundle install <bundle-id>`
|
||||
- If your bundle depends on components from non-default catalogs, document those catalog URLs and test installation from a clean project
|
||||
|
||||
- type: input
|
||||
id: bundle-id
|
||||
attributes:
|
||||
label: Bundle ID
|
||||
description: Unique bundle identifier; must start and end with a lowercase letter or digit and may contain lowercase letters, digits, dots, underscores, and hyphens between
|
||||
placeholder: "e.g., security-governance-stack"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: bundle-name
|
||||
attributes:
|
||||
label: Bundle Name
|
||||
description: Human-readable bundle name
|
||||
placeholder: "e.g., Security Governance Stack"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: Semantic version number
|
||||
placeholder: "e.g., 1.0.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: role
|
||||
attributes:
|
||||
label: Role or Team
|
||||
description: Primary role, team, or persona this bundle provisions
|
||||
placeholder: "e.g., security-engineer, product-manager, platform-team"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Brief description of the stack this bundle installs
|
||||
placeholder: Installs a security governance stack with compliance presets, review commands, and evidence workflows
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: author
|
||||
attributes:
|
||||
label: Author
|
||||
description: Your name or organization
|
||||
placeholder: "e.g., Jane Doe or Acme Corp"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: repository
|
||||
attributes:
|
||||
label: Repository URL
|
||||
description: GitHub repository URL for your bundle source
|
||||
placeholder: "https://github.com/your-org/spec-kit-bundle-your-bundle"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: download-url
|
||||
attributes:
|
||||
label: Download URL
|
||||
description: URL to the versioned bundle artifact generated by `specify bundle build`
|
||||
placeholder: "https://github.com/your-org/spec-kit-bundle-your-bundle/releases/download/v1.0.0/your-bundle-1.0.0.zip"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: documentation
|
||||
attributes:
|
||||
label: Documentation URL
|
||||
description: Link to documentation that explains what the bundle installs and how to use it
|
||||
placeholder: "https://github.com/your-org/spec-kit-bundle-your-bundle/blob/main/README.md"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: license
|
||||
attributes:
|
||||
label: License
|
||||
description: Open source license type
|
||||
placeholder: "e.g., MIT, Apache-2.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: speckit-version
|
||||
attributes:
|
||||
label: Required Spec Kit Version
|
||||
description: Minimum Spec Kit version required by the bundle
|
||||
placeholder: "e.g., >=0.9.0"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: integration
|
||||
attributes:
|
||||
label: Integration Target (optional)
|
||||
description: Integration ID if the bundle pins one; leave empty if integration-agnostic
|
||||
placeholder: "e.g., claude, copilot, gemini"
|
||||
|
||||
- type: textarea
|
||||
id: components-provided
|
||||
attributes:
|
||||
label: Components Provided
|
||||
description: List the extensions, presets, workflows, and steps this bundle installs
|
||||
placeholder: |
|
||||
- extensions: sicario-guard@0.5.1
|
||||
- presets: sicario-core@0.5.1, sicario-ai-governance@0.5.1
|
||||
- workflows: evidence-review@1.0.0
|
||||
- steps: threat-model
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: required-catalogs
|
||||
attributes:
|
||||
label: Required Component Catalogs
|
||||
description: List any non-default catalogs users must add before this bundle can resolve its components; enter "None" if every component resolves from built-in or bundled catalogs
|
||||
placeholder: |
|
||||
- Presets: https://github.com/your-org/your-bundle/releases/download/v1.0.0/presets.json
|
||||
- Extensions: https://github.com/your-org/your-bundle/releases/download/v1.0.0/extensions.json
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: tags
|
||||
attributes:
|
||||
label: Tags
|
||||
description: 2-5 relevant tags (lowercase, separated by commas)
|
||||
placeholder: "security, governance, compliance"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: features
|
||||
attributes:
|
||||
label: Key Features
|
||||
description: List the main capabilities this bundle provides
|
||||
placeholder: |
|
||||
- Installs evidence-first security governance templates
|
||||
- Adds automated bundle verification commands
|
||||
- Pins all components to release-tested versions
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: testing
|
||||
attributes:
|
||||
label: Testing Checklist
|
||||
description: Confirm that your bundle has been tested
|
||||
options:
|
||||
- label: Validation succeeds with `specify bundle validate --path <bundle-directory>`
|
||||
required: true
|
||||
- label: Build succeeds with `specify bundle build --path <bundle-directory>` and produces the submitted artifact
|
||||
required: true
|
||||
- label: Bundle installs successfully from the built artifact
|
||||
required: true
|
||||
- label: The submitted distribution path was tested end to end, including bundle-ID installation from an install-allowed catalog when a catalog entry is proposed
|
||||
required: true
|
||||
- label: Installation was tested in a clean Spec Kit project
|
||||
required: true
|
||||
- label: Required component catalogs are documented and were included in testing, or no extra catalogs are required
|
||||
required: true
|
||||
- label: Documentation is complete and accurate
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: requirements
|
||||
attributes:
|
||||
label: Submission Requirements
|
||||
description: Verify your bundle meets all requirements
|
||||
options:
|
||||
- label: Valid `bundle.yml` manifest included
|
||||
required: true
|
||||
- label: README.md explains the bundle's intended role, installed components, and installation steps
|
||||
required: true
|
||||
- label: LICENSE file included
|
||||
required: true
|
||||
- label: GitHub release created with a version tag
|
||||
required: true
|
||||
- label: Bundle ID matches the manifest and follows naming conventions
|
||||
required: true
|
||||
- label: Every extension, preset, workflow, and step reference is pinned where the manifest requires a version
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: testing-details
|
||||
attributes:
|
||||
label: Testing Details
|
||||
description: Describe how you tested your bundle
|
||||
placeholder: |
|
||||
**Tested on:**
|
||||
- macOS 15 with Spec Kit v0.9.0
|
||||
- Ubuntu 24.04 with Spec Kit v0.9.0
|
||||
|
||||
**Test project:** [Link or description]
|
||||
|
||||
**Test scenarios:**
|
||||
1. Added required catalogs
|
||||
2. Validated bundle manifest
|
||||
3. Built release artifact
|
||||
4. Installed bundle in a clean project
|
||||
5. Ran the installed commands or workflows
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: example-usage
|
||||
attributes:
|
||||
label: Example Usage
|
||||
description: Provide a simple example of installing and using your bundle
|
||||
render: markdown
|
||||
placeholder: |
|
||||
```bash
|
||||
# Add any required component catalogs first
|
||||
specify preset catalog add https://github.com/your-org/your-bundle/releases/download/v1.0.0/presets.json --name your-bundle --install-allowed
|
||||
specify extension catalog add https://github.com/your-org/your-bundle/releases/download/v1.0.0/extensions.json --name your-bundle --install-allowed
|
||||
|
||||
# Install the downloaded bundle artifact
|
||||
curl -L -o your-bundle-1.0.0.zip https://github.com/your-org/your-bundle/releases/download/v1.0.0/your-bundle-1.0.0.zip
|
||||
specify bundle install ./your-bundle-1.0.0.zip
|
||||
|
||||
# Or test through an install-allowed bundle catalog
|
||||
specify bundle catalog add https://github.com/your-org/your-bundle/releases/download/v1.0.0/bundles.json --id your-bundle-catalog --policy install-allowed
|
||||
specify bundle install your-bundle
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: catalog-entry
|
||||
attributes:
|
||||
label: Proposed Catalog Entry
|
||||
description: Provide the JSON entry that would appear under the top-level `bundles` object in a bundle catalog (helps reviewers)
|
||||
render: json
|
||||
placeholder: |
|
||||
{
|
||||
"your-bundle": {
|
||||
"name": "Your Bundle",
|
||||
"id": "your-bundle",
|
||||
"version": "1.0.0",
|
||||
"role": "security-engineer",
|
||||
"description": "Brief description of the stack",
|
||||
"author": "Your Name",
|
||||
"license": "MIT",
|
||||
"download_url": "https://github.com/your-org/your-bundle/releases/download/v1.0.0/your-bundle-1.0.0.zip",
|
||||
"repository": "https://github.com/your-org/your-bundle",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.9.0"
|
||||
},
|
||||
"provides": {
|
||||
"extensions": 1,
|
||||
"presets": 2,
|
||||
"steps": 0,
|
||||
"workflows": 1
|
||||
},
|
||||
"tags": ["security", "governance"],
|
||||
"verified": false
|
||||
}
|
||||
}
|
||||
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, dependency-resolution notes, etc.
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -72,21 +72,23 @@ body:
|
||||
- Goose
|
||||
- Hermes Agent
|
||||
- IBM Bob
|
||||
- iFlow CLI
|
||||
- Junie
|
||||
- Kilo Code
|
||||
- Kimi Code
|
||||
- Kiro CLI
|
||||
- Lingma
|
||||
- Mistral Vibe
|
||||
- Oh My Pi
|
||||
- opencode
|
||||
- Pi Coding Agent
|
||||
- Qoder CLI
|
||||
- Qwen Code
|
||||
- Roo Code
|
||||
- RovoDev ACLI
|
||||
- SHAI
|
||||
- Tabnine CLI
|
||||
- Trae
|
||||
- Windsurf
|
||||
- ZCode
|
||||
- Zed
|
||||
- Not applicable
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/preset_submission.yml
vendored
14
.github/ISSUE_TEMPLATE/preset_submission.yml
vendored
@@ -77,18 +77,6 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: documentation
|
||||
attributes:
|
||||
label: Documentation URL
|
||||
description: |
|
||||
Link to the README that explains how to use **this preset** (not a general product/framework pitch).
|
||||
Prefer the preset-scoped README (e.g. `presets/<id>/README.md` in a monorepo) over the repository root README.
|
||||
It must contain at least one valid `specify preset add ...` install command — ideally `specify preset add --from <download-url>` using the exact Download URL above (other forms such as `specify preset add <preset-id>` or `specify preset add --dev <path>` are also accepted).
|
||||
placeholder: "https://github.com/your-org/spec-kit-presets/blob/main/presets/your-preset/README.md"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: license
|
||||
attributes:
|
||||
@@ -187,7 +175,7 @@ body:
|
||||
options:
|
||||
- label: Valid `preset.yml` manifest included
|
||||
required: true
|
||||
- label: Linked README (Documentation URL) explains how to use this preset and includes a valid `specify preset add ...` command (preferably `specify preset add --from <download-url>` using the exact download URL)
|
||||
- label: README.md with description and usage instructions
|
||||
required: true
|
||||
- label: LICENSE file included
|
||||
required: true
|
||||
|
||||
2
.github/workflows/add-community-preset.lock.yml
generated
vendored
2
.github/workflows/add-community-preset.lock.yml
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4ba1db5fdec754fa825cc3160879924118bc454a781eed70ef6c90beab83a95","body_hash":"cb6c19088fa13da0a8320c174e8c14c4887d2c8a005a5cb2d2d2faa3f890de39","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
|
||||
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b4ba1db5fdec754fa825cc3160879924118bc454a781eed70ef6c90beab83a95","body_hash":"392ace500b7cb9b0aa6b020d150841de398bcbcfe54dbad729f0d860d698bde2","compiler_version":"v0.79.8","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c0338fef4749d08c21f8f975fb0e37efa17dda47","version":"v0.79.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
|
||||
# This file was automatically generated by gh-aw (v0.79.8). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
|
||||
#
|
||||
|
||||
62
.github/workflows/add-community-preset.md
vendored
62
.github/workflows/add-community-preset.md
vendored
@@ -73,7 +73,6 @@ fields):
|
||||
| Author | `author` | Yes |
|
||||
| Repository URL | `repository` | Yes |
|
||||
| Download URL | `download-url` | Yes |
|
||||
| Documentation URL | `documentation` | Yes |
|
||||
| License | `license` | Yes |
|
||||
| Required Spec Kit Version | `speckit-version` | Yes |
|
||||
| Required Extensions | `required-extensions` | No |
|
||||
@@ -101,70 +100,17 @@ deciding pass/fail:
|
||||
### 2c. Repository validation
|
||||
- Fetch the repository URL — confirm it exists and is publicly accessible
|
||||
- Confirm the repository contains a `preset.yml` file
|
||||
- Confirm the repository contains a `README.md` file
|
||||
- Confirm the repository contains a `LICENSE` file
|
||||
|
||||
> The README requirement is enforced once, in **Step 2d**, against the specific file the
|
||||
> `documentation` field points to — not a generic repository-root `README.md`. This avoids
|
||||
> the monorepo false-positive where a root README exists but isn't the preset-usage doc.
|
||||
|
||||
### 2d. Documentation README validation
|
||||
|
||||
The `documentation` field must point to the README that explains **how to use this
|
||||
preset** — not just any file named `README.md`, and not a product/framework pitch.
|
||||
|
||||
- **Restrict the URL to GitHub before fetching.** The `documentation` value is
|
||||
user-provided input. Only accept GitHub-hosted README URLs:
|
||||
- `https://github.com/<owner>/<repo>/blob/<ref>/<path>`
|
||||
- `https://github.com/<owner>/<repo>/raw/<ref>/<path>`
|
||||
- `https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>`
|
||||
|
||||
If the URL points anywhere else (or isn't a URL), **fail this check** and do not fetch it.
|
||||
- **Require the URL to point at a README file.** After stripping any fragment/query (see
|
||||
below), the URL path must end with `README.md` (case-insensitive). If it points at some
|
||||
other Markdown file, **fail this check** and ask the submitter to link the preset's README.
|
||||
- Fetch the **exact URL** in the `documentation` field. First strip any fragment (`#...`)
|
||||
or query string (`?...`) — these are common when copying from the browser UI and must be
|
||||
ignored so the fetch target is deterministic. Then resolve the raw content to fetch:
|
||||
- For a `github.com/<owner>/<repo>/blob/<ref>/<path>` URL, fetch the equivalent
|
||||
`github.com/<owner>/<repo>/raw/<ref>/<path>` URL (only swap `/blob/` → `/raw/`).
|
||||
- Fetch `github.com/.../raw/...` and `raw.githubusercontent.com/...` URLs as-is.
|
||||
|
||||
Do **not** rewrite into `raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>` form — that
|
||||
format can't reliably represent refs containing slashes (e.g. a `feature/foo` branch).
|
||||
Confirm the fetched URL resolves to a readable Markdown file.
|
||||
- **Validate that the README contains a valid Spec Kit CLI install command.** The fetched
|
||||
README must contain at least one `specify preset add ...` invocation. The strongest
|
||||
signal is the catalog-install form whose URL matches the submitted **Download URL**:
|
||||
- `specify preset add --from <download-url>` (preferred), or
|
||||
- `specify preset add <preset-id>`, or
|
||||
- `specify preset add --dev <path>`
|
||||
|
||||
A `specify preset add --from <url>` command only counts when its `<url>` **matches the
|
||||
submitted Download URL exactly**. A `--from` command pointing at a *different* URL does
|
||||
**not** satisfy the install-command requirement (treat it as if absent) — but the README
|
||||
may still pass on one of the other accepted forms (`specify preset add <preset-id>` or
|
||||
`specify preset add --dev <path>`).
|
||||
|
||||
If **no** accepted `specify preset add ...` command is present, the README is treated as a
|
||||
generic description/pitch rather than preset-usage documentation — **fail this check** and
|
||||
tell the submitter to add a valid install command (ideally
|
||||
`specify preset add --from <download-url>`).
|
||||
- **Prefer a preset-scoped README in monorepos.** If `documentation` resolves to a generic
|
||||
repository-root README in a monorepo (the preset lives in a subdirectory such as
|
||||
`presets/<id>/` and a preset-scoped README exists there), **flag it** in your comment and
|
||||
recommend the submitter point `documentation` at the preset-scoped README
|
||||
(e.g. `presets/<id>/README.md`) so the catalog surfaces usage instead of marketing. Treat
|
||||
this as a flag rather than a hard failure **only if** the root README still contains a valid
|
||||
`specify preset add ...` command for this preset; otherwise it fails check 2d above.
|
||||
|
||||
### 2e. Release and download URL validation
|
||||
### 2d. Release and download URL validation
|
||||
- The download URL should follow the pattern
|
||||
`https://github.com/<owner>/<repo>/archive/refs/tags/v<version>.zip`
|
||||
or
|
||||
`https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>.zip`
|
||||
- Verify a GitHub release exists matching the submitted version
|
||||
|
||||
### 2f. Submission checklists
|
||||
### 2e. Submission checklists
|
||||
- Confirm that all required checkboxes in the Testing Checklist and Submission
|
||||
Requirements sections are checked (`[x]`)
|
||||
|
||||
@@ -208,7 +154,7 @@ Insert the entry in **alphabetical order by preset ID** within the
|
||||
"repository": "<repository>",
|
||||
"download_url": "<download_url>",
|
||||
"homepage": "<homepage or repository>",
|
||||
"documentation": "<documentation URL — the validated preset-usage README>",
|
||||
"documentation": "<documentation or repository README>",
|
||||
"license": "<license>",
|
||||
"requires": {
|
||||
"speckit_version": "<speckit_version>"
|
||||
|
||||
2
.github/workflows/catalog-assign.yml
vendored
2
.github/workflows/catalog-assign.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const issue = context.payload.issue;
|
||||
|
||||
25
.github/workflows/lint.yml
vendored
25
.github/workflows/lint.yml
vendored
@@ -42,28 +42,3 @@ jobs:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
!extensions/**/*.md
|
||||
|
||||
shellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
# shellcheck is preinstalled on ubuntu-latest runners.
|
||||
# Start at --severity=error to block real bugs without flagging style
|
||||
# (notably SC2155). Tighten in a follow-up after cleanup.
|
||||
- name: Run shellcheck on shell scripts
|
||||
run: git ls-files -z -- '*.sh' | xargs -0 shellcheck --severity=error
|
||||
|
||||
# macOS ships bash 3.2, where bash 4+ case-modification parameter
|
||||
# expansions error with "bad substitution". shellcheck assumes bash 4+
|
||||
# from the shebang and cannot flag these, so guard explicitly; use tr
|
||||
# for portable case conversion.
|
||||
- name: Reject bash 4+ case-modification expansions
|
||||
run: |
|
||||
matches=$(git ls-files -z -- '*.sh' | xargs -0 grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*(\[[^]]*\])?(\^\^?|,,?|~~?|@[UuLl])[^}]*\}' || true)
|
||||
if [ -n "$matches" ]; then
|
||||
echo "Found bash 4+ case-modification expansion(s); use tr for portability (macOS ships bash 3.2):"
|
||||
echo "$matches"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -19,9 +19,9 @@ jobs:
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Run ruff check
|
||||
run: uvx ruff check src/
|
||||
@@ -30,8 +30,8 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ["3.13", "3.14"]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
41
AGENTS.md
41
AGENTS.md
@@ -23,7 +23,7 @@ src/specify_cli/integrations/
|
||||
│ └── __init__.py # ClaudeIntegration class
|
||||
├── gemini/ # Example: TomlIntegration subclass
|
||||
│ └── __init__.py
|
||||
├── kilocode/ # Example: MarkdownIntegration subclass
|
||||
├── windsurf/ # Example: MarkdownIntegration subclass
|
||||
│ └── __init__.py
|
||||
├── copilot/ # Example: IntegrationBase subclass (custom setup)
|
||||
│ └── __init__.py
|
||||
@@ -52,29 +52,30 @@ Most agents only need `MarkdownIntegration` — a minimal subclass with zero met
|
||||
|
||||
Create `src/specify_cli/integrations/<package_dir>/__init__.py`, where `<package_dir>` is the Python-safe directory name derived from `<key>`: use the key as-is when it contains no hyphens (e.g., key `"gemini"` → `gemini/`), or replace hyphens with underscores when it does (e.g., key `"kiro-cli"` → `kiro_cli/`). The `IntegrationBase.key` class attribute always retains the original hyphenated value, since that is what the CLI and registry use. For CLI-based integrations (`requires_cli: True`), the `key` should match the actual CLI tool name (the executable users install and run) so CLI checks can resolve it correctly. For IDE-based integrations (`requires_cli: False`), use the canonical integration identifier instead.
|
||||
|
||||
**Minimal example — Markdown agent (Kilo Code):**
|
||||
**Minimal example — Markdown agent (Windsurf):**
|
||||
|
||||
```python
|
||||
"""Kilo Code IDE integration."""
|
||||
"""Windsurf IDE integration."""
|
||||
|
||||
from ..base import MarkdownIntegration
|
||||
|
||||
|
||||
class KilocodeIntegration(MarkdownIntegration):
|
||||
key = "kilocode"
|
||||
class WindsurfIntegration(MarkdownIntegration):
|
||||
key = "windsurf"
|
||||
config = {
|
||||
"name": "Kilo Code",
|
||||
"folder": ".kilocode/",
|
||||
"name": "Windsurf",
|
||||
"folder": ".windsurf/",
|
||||
"commands_subdir": "workflows",
|
||||
"install_url": None,
|
||||
"requires_cli": False,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".kilocode/workflows",
|
||||
"dir": ".windsurf/workflows",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = ".windsurf/rules/specify-rules.md"
|
||||
```
|
||||
|
||||
**TOML agent (Gemini):**
|
||||
@@ -100,6 +101,7 @@ class GeminiIntegration(TomlIntegration):
|
||||
"args": "{{args}}",
|
||||
"extension": ".toml",
|
||||
}
|
||||
context_file = "GEMINI.md"
|
||||
```
|
||||
|
||||
**Skills agent (Codex):**
|
||||
@@ -127,6 +129,7 @@ class CodexIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
@@ -147,8 +150,9 @@ class CodexIntegration(SkillsIntegration):
|
||||
| `key` | Class attribute | Unique identifier; for CLI-based integrations (`requires_cli: True`), must match the CLI executable name |
|
||||
| `config` | Class attribute (dict) | Agent metadata: `name`, `folder`, `commands_subdir`, `install_url`, `requires_cli` |
|
||||
| `registrar_config` | Class attribute (dict) | Command output config: `dir`, `format`, `args` placeholder, file `extension` |
|
||||
| `context_file` | Class attribute (str or None) | Path to agent context/instructions file (e.g., `"CLAUDE.md"`, `".github/copilot-instructions.md"`) |
|
||||
|
||||
**Key design rule:** For CLI-based integrations (`requires_cli: True`), `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings. IDE-based integrations (`requires_cli: False`) should use their canonical identifier (e.g., `"kilocode"`, `"copilot"`).
|
||||
**Key design rule:** For CLI-based integrations (`requires_cli: True`), `key` must be the actual executable name (e.g., `"cursor-agent"` not `"cursor"`). This ensures `shutil.which(key)` works for CLI-tool checks without special-case mappings. IDE-based integrations (`requires_cli: False`) should use their canonical identifier (e.g., `"windsurf"`, `"copilot"`).
|
||||
|
||||
### 3. Register it
|
||||
|
||||
@@ -171,11 +175,9 @@ def _register_builtins() -> None:
|
||||
|
||||
### 4. Context file behavior
|
||||
|
||||
The Specify CLI carries **no agent-context state whatsoever**. Integration classes do **not** declare a `context_file`, and the CLI never creates, updates, removes, resolves, or migrates a context/instruction file (`CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, …). New integrations add nothing for context handling.
|
||||
Set `context_file` on the integration class. The base integration setup creates or updates the managed Spec Kit section in that file, and uninstall removes the managed section when appropriate.
|
||||
|
||||
Managing the "Spec Kit" section in the context file is fully owned by the bundled `agent-context` extension (`extensions/agent-context/`), which is a **full opt-in**: `specify init` does not install it. A user adds/enables it through the standard extension verbs, after which the extension's own bundled scripts maintain the context section. When the extension is absent or disabled, nothing in Spec Kit touches the context file.
|
||||
|
||||
The extension reads its own config file at `.specify/extensions/agent-context/agent-context-config.yml`:
|
||||
The managed section is owned by the bundled `agent-context` extension (`extensions/agent-context/`). All configuration flows through the extension's own config file at `.specify/extensions/agent-context/agent-context-config.yml`:
|
||||
|
||||
```yaml
|
||||
# Path to the coding agent context file managed by this extension
|
||||
@@ -187,10 +189,10 @@ context_markers:
|
||||
end: "<!-- SPECKIT END -->"
|
||||
```
|
||||
|
||||
- The Specify CLI does **not** write this config. When `context_file` is empty, the extension's bundled scripts self-seed it by looking up the active integration's key in the extension's own `agent-context-defaults.json` map (`extensions/agent-context/scripts/bash/update-agent-context.sh` and `.ps1`). The CLI registry is never consulted — all agent→context-file knowledge lives inside the extension.
|
||||
- `context_markers.{start,end}` are read solely by the extension's scripts; they default to the Spec Kit markers shown above and can be customized by editing `agent-context-config.yml` directly.
|
||||
- `context_file` is written automatically from the integration's class attribute when `specify init` or `specify integration use` is run.
|
||||
- `context_markers.{start,end}` defaults to `IntegrationBase.CONTEXT_MARKER_START` / `CONTEXT_MARKER_END`. Users who want custom markers edit `agent-context-config.yml` directly — both the Python layer (`upsert_context_section()` / `remove_context_section()`) and the bundled scripts (`extensions/agent-context/scripts/bash/update-agent-context.sh` and `.ps1`) read from this single source of truth.
|
||||
|
||||
Existing projects created by older Spec Kit versions keep working: any previously written managed section or extension config is left intact and is only ever updated by the extension when run.
|
||||
Users can opt out entirely with `specify extension disable agent-context`; while disabled, Spec Kit skips context-file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
|
||||
|
||||
Only add custom setup logic when the agent needs non-standard behavior. Integrations no longer require per-agent thin wrapper scripts or shared context-update dispatcher scripts — the `agent-context` extension is fully generic.
|
||||
|
||||
@@ -201,8 +203,8 @@ Only add custom setup logic when the agent needs non-standard behavior. Integrat
|
||||
specify init my-project --integration <key>
|
||||
|
||||
# Verify files were created in the commands directory configured by
|
||||
# config["folder"] + config["commands_subdir"] (for example, .kilocode/workflows/)
|
||||
ls -R my-project/.kilocode/workflows/
|
||||
# config["folder"] + config["commands_subdir"] (for example, .windsurf/workflows/)
|
||||
ls -R my-project/.windsurf/workflows/
|
||||
|
||||
# Uninstall cleanly
|
||||
cd my-project && specify integration uninstall <key>
|
||||
@@ -399,6 +401,7 @@ Implementation: Extends `YamlIntegration` (parallel to `TomlIntegration`):
|
||||
2. Extracts title and description from frontmatter
|
||||
3. Renders output as Goose recipe YAML (version, title, description, author, extensions, activities, prompt)
|
||||
4. Uses `yaml.safe_dump()` for header fields to ensure proper escaping
|
||||
5. Sets `context_file = "AGENTS.md"` so the base setup manages the Spec Kit context section there
|
||||
|
||||
## Branch Naming Convention
|
||||
|
||||
@@ -463,7 +466,7 @@ Disclosure is **continuous**, not a one-time event. A single AI-disclosure parag
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Using shorthand keys for CLI-based integrations**: For CLI-based integrations (`requires_cli: True`), the `key` must match the executable name (e.g., `"cursor-agent"` not `"cursor"`). `shutil.which(key)` is used for CLI tool checks — mismatches require special-case mappings. IDE-based integrations (`requires_cli: False`) are not subject to this constraint.
|
||||
2. **Reintroducing context handling into the CLI**: The opt-in `agent-context` extension owns everything about context files — including the per-agent default mapping in `agent-context-defaults.json`. Integration classes must **not** declare a `context_file`, and no CLI code should read, write, resolve, or migrate context files. All context-file logic lives in `.specify/extensions/agent-context/` and its bundled scripts.
|
||||
2. **Forgetting context configuration**: The bundled `agent-context` extension reads from `.specify/extensions/agent-context/agent-context-config.yml`. New integrations only need to set `context_file` on the class — markers and dispatcher scripts are managed centrally.
|
||||
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that have a CLI tool; set to `False` for IDE-based agents.
|
||||
4. **Wrong argument format**: Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML agents.
|
||||
5. **Skipping registration**: The import and `_register()` call in `_register_builtins()` must both be added.
|
||||
|
||||
126
CHANGELOG.md
126
CHANGELOG.md
@@ -2,132 +2,6 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.12.3] - 2026-07-01
|
||||
|
||||
### Changed
|
||||
|
||||
- feat(copilot): warn before skills default rollout (#3256)
|
||||
- Add June 2026 newsletter (#3289)
|
||||
- docs(toc): add Bundles and Authentication to the Reference nav (#3267)
|
||||
- fix(integrations): add zed to discovery catalog.json (#3266)
|
||||
- fix(integrations): cline hook note collapses onto instruction at EOF (#3263)
|
||||
- refactor: move workflow command handlers to workflows/_commands.py (PR-8/8) (#3159)
|
||||
- chore: retire Roo Code integration — extension shut down (#3167) (#3212)
|
||||
- fix(bundle): allow 'catalog remove' by the same relative path used to add (#3242)
|
||||
- fix(workflows): reject bool max_iterations in while/do-while validation (#3237)
|
||||
- fix: allow prerelease spec-kit versions in compatibility checks (#2695)
|
||||
- chore: release 0.12.2, begin 0.12.3.dev0 development (#3259)
|
||||
|
||||
## [0.12.2] - 2026-06-30
|
||||
|
||||
### Changed
|
||||
|
||||
- fix(scripts): portable uppercase for branch-name acronym retention (bash 3.2) (#3192)
|
||||
- chore: retire Windsurf integration — absorbed into Cognition Devin (#3168) (#3213)
|
||||
- [extension] Update Intake extension to v0.1.3 (#3254)
|
||||
- feat(workflows): honor max_concurrency in fan-out via a bounded thread pool (#3224)
|
||||
- Update Architecture Workflow extension to v1.2.2 (#3255)
|
||||
- Add Repository Governance extension to community catalog (#3252)
|
||||
- Update Workflow Preset to v1.3.11 (#3251)
|
||||
- chore: retire iflow integration — product discontinued (#3166) (#3211)
|
||||
- docs(codebuddy): fix dead install links and CodeBuddy capitalization (#3172) (#3216)
|
||||
- fix: reject host-less catalog URLs in base and preset validators (#3209) (#3227)
|
||||
- chore: release 0.12.1, begin 0.12.2.dev0 development (#3253)
|
||||
|
||||
## [0.12.1] - 2026-06-30
|
||||
|
||||
### Changed
|
||||
|
||||
- chore: align CI Python matrix with devguide lifecycle + fix bash 3.2 portability (#3244)
|
||||
- fix: stop check-prerequisites --paths-only from writing feature.json (#3025) (#3190)
|
||||
- docs: document integration catalog subcommands (#3206)
|
||||
- fix(scripts): use ASCII [OK] marker in initialize-repo.sh (parity with PowerShell twin) (#3231)
|
||||
- docs: document integration `search`/`info`/`scaffold` subcommands (#3174) (#3194)
|
||||
- docs: remove Cursor from `specify check` agent list (#3178) (#3193)
|
||||
- fix(goose): repoint install_url and docs to goose-docs.ai (#3171) (#3215)
|
||||
- fix(scripts): route 'Plan template not found' per --json in setup-plan.ps1 (parity with bash) (#3241)
|
||||
- fix(bundle): send command errors to stderr so --json stdout stays parseable (#3235)
|
||||
- chore: release 0.12.0, begin 0.12.1.dev0 development (#3243)
|
||||
|
||||
## [0.12.0] - 2026-06-29
|
||||
|
||||
### Changed
|
||||
|
||||
- feat: make agent-context extension a full opt-in (#3097)
|
||||
- docs(workflows): add the built-in 'init' step type to the Step Types table (#3234)
|
||||
- fix(workflows): gate validate() must not crash on non-string options (#3233)
|
||||
- fix(workflows): make pipe-filter detection quote-aware in expressions (#3232)
|
||||
- fix(workflows): reject a fan-in wait_for that names an unknown step at validation (#3225)
|
||||
- fix(scripts): warn when spec template is missing in create-new-feature.ps1 (parity with bash) (#3230)
|
||||
- fix(scripts): count subdirectory-only dirs as non-empty in PowerShell (parity with bash) (#3137)
|
||||
- fix(scripts): drop HAS_GIT from PowerShell git-extension output (parity with bash) (#3195)
|
||||
- Update Product Spec Extension to v1.0.1 (#3226)
|
||||
- chore: release 0.11.10, begin 0.11.11.dev0 development (#3240)
|
||||
|
||||
## [0.11.10] - 2026-06-29
|
||||
|
||||
### Changed
|
||||
|
||||
- fix(extensions): apply GHES auth and resolve release assets for `extension add --from` (#3217)
|
||||
- fix(pi): repoint install_url to @earendil-works/pi-coding-agent (#3169) (#3214)
|
||||
- fix(catalogs): reject host-less catalog URLs in base and preset validators (#3210)
|
||||
- fix: update CodeBuddy install docs URL (#3187)
|
||||
- fix(workflows): reject infinite number-input default instead of raising OverflowError (#3199)
|
||||
- fix(scripts): emit 'Copied plan template' status in setup-plan.ps1 (parity with bash) (#3198)
|
||||
- fix(workflows): make expression operator/literal parsing quote-aware (#3197)
|
||||
- fix(scripts): honor explicit -Number 0 in PowerShell create-new-feature (parity with bash) (#3196)
|
||||
- Add community bundle submission path (#3162)
|
||||
- Docs: Document /speckit.converge command (#3181)
|
||||
- chore: release 0.11.9, begin 0.11.10.dev0 development (#3189)
|
||||
|
||||
## [0.11.9] - 2026-06-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Docs: add cline and zcode to multi-install-safe table (#3180)
|
||||
- Docs: document missing flags --force and --refresh-shared-infra (#3179)
|
||||
- fix(claude): stop forking /speckit-analyze to prevent long-session freezes (#3188)
|
||||
- fix: derive plan path from feature.json in update-agent-context (#3069)
|
||||
- fix(catalog): companion → README docs, version-pinned download URL, v0.11.0, refreshed tags (#2954)
|
||||
- chore(deps): bump actions/setup-python from 6.2.0 to 6.3.0 (#3173)
|
||||
- Update SicarioSpec Core preset to v0.5.1 (#3165)
|
||||
- fix(extensions,presets,workflows): resolve private GHES release assets via /api/v3 (#3157)
|
||||
- Update preset composition strategy reference (#3143)
|
||||
- fix(scripts): keep PowerShell branch-name acronym match case-sensitive (parity with bash) (#3129)
|
||||
- fix(extensions): tell agent to run mandatory hooks, not just emit the directive (#2901)
|
||||
- Point sicario-core docs to preset README (#3120)
|
||||
- chore: release 0.11.8, begin 0.11.9.dev0 development (#3156)
|
||||
|
||||
## [0.11.8] - 2026-06-24
|
||||
|
||||
### Changed
|
||||
|
||||
- docs: add SpecKit Assistant npm package to Community Friends (#3142)
|
||||
- Require preset-usage README with Spec Kit CLI syntax in preset submissions (#3104)
|
||||
- [extension] Update Jira Integration (Sync Engine) extension to v0.4.0 (#3152)
|
||||
- Add Spec Roadmap extension to community catalog (#3153)
|
||||
- feat(integration): update Kimi integration for Kimi Code CLI (#2979)
|
||||
- [extension] Add Golden Demo extension to community catalog (#3151)
|
||||
- docs: run /speckit.checklist after /speckit.plan in quickstart (#3108)
|
||||
- fix(workflows): preserve commas inside quoted list-literal elements (#3134)
|
||||
- ci: pin actions to commit SHAs and add shellcheck (#3126)
|
||||
- chore: release 0.11.7, begin 0.11.8.dev0 development (#3154)
|
||||
|
||||
## [0.11.7] - 2026-06-24
|
||||
|
||||
### Changed
|
||||
|
||||
- feat(extensions): verify catalog archive sha256 before install (#3080)
|
||||
- fix(workflows): validate requires keys and reject phantom permissions gate (#3079)
|
||||
- fix(scripts): use case-sensitive match for acronym retention in PS branch names (#3130)
|
||||
- feat(integrations): add omp support (#3107)
|
||||
- fix: render valid TOML when a command body contains backslashes (#3135)
|
||||
- harden: reject shell=True in run_command (#3132)
|
||||
- docs: add monorepo guide (#3084)
|
||||
- fix(scripts): send check-prerequisites.ps1 errors to stderr (#3123)
|
||||
- fix: write Codex dev skills as files (#2988)
|
||||
- chore: release 0.11.6, begin 0.11.7.dev0 development (#3121)
|
||||
|
||||
## [0.11.6] - 2026-06-23
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -113,16 +113,6 @@ uv pip install -e ".[test]"
|
||||
> `specify_cli` to this checkout's `src/`. This matches the gotcha documented in
|
||||
> `AGENTS.md` (Common Pitfalls).
|
||||
|
||||
#### Shell scripts
|
||||
|
||||
```bash
|
||||
git ls-files -z -- '*.sh' | xargs -0 shellcheck --severity=error
|
||||
```
|
||||
|
||||
The CI `lint.yml` `shellcheck` job currently reports and blocks only
|
||||
error-severity findings. Warnings such as SC2155 are intentionally outside this
|
||||
job until a follow-up cleanup tightens the threshold.
|
||||
|
||||
### Manual testing
|
||||
|
||||
#### Testing setup
|
||||
|
||||
11
README.md
11
README.md
@@ -134,14 +134,13 @@ Explore community-contributed resources on the [Spec Kit docs site](https://gith
|
||||
|
||||
- [Extensions](https://github.github.io/spec-kit/community/extensions.html) — commands, hooks, and capabilities
|
||||
- [Presets](https://github.github.io/spec-kit/community/presets.html) — template and terminology overrides
|
||||
- [Bundles](https://github.github.io/spec-kit/community/bundles.html) — role and team stacks composed from existing components
|
||||
- [Walkthroughs](https://github.github.io/spec-kit/community/walkthroughs.html) — end-to-end SDD scenarios
|
||||
- [Friends](https://github.github.io/spec-kit/community/friends.html) — projects that extend or build on Spec Kit
|
||||
|
||||
> [!NOTE]
|
||||
> Community contributions are independently created and maintained by their respective authors. Review source code before installation and use at your own discretion.
|
||||
|
||||
Want to contribute? See the [Extension Publishing Guide](extensions/EXTENSION-PUBLISHING-GUIDE.md), the [Presets Publishing Guide](presets/PUBLISHING.md), or the [Community Bundles guide](docs/community/bundles.md).
|
||||
Want to contribute? See the [Extension Publishing Guide](extensions/EXTENSION-PUBLISHING-GUIDE.md) or the [Presets Publishing Guide](presets/PUBLISHING.md).
|
||||
|
||||
## 🤖 Supported AI Coding Agent Integrations
|
||||
|
||||
@@ -263,10 +262,8 @@ built-in). Each source carries an install policy: `install-allowed` sources can
|
||||
be installed from, while `discovery-only` sources are visible in `search`/`info`
|
||||
but refuse installation. Manage the stack with `specify bundle catalog list|add|remove`.
|
||||
|
||||
Authors validate and package bundles locally. Distribution is hosting the built
|
||||
artifact and adding a catalog source; community bundle submissions use the
|
||||
[Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml)
|
||||
issue template so required component catalogs and install evidence can be reviewed:
|
||||
Authors validate and package bundles locally — there is no first-class publish;
|
||||
distribution is hosting the built artifact and adding a catalog entry:
|
||||
|
||||
```bash
|
||||
specify bundle validate --path ./my-bundle # structural + reference checks
|
||||
@@ -406,7 +403,7 @@ specify init . --force --integration copilot
|
||||
specify init --here --force --integration copilot
|
||||
```
|
||||
|
||||
The CLI will check that your selected agent's CLI tool is installed (for integrations that require a CLI), such as Claude Code, Gemini CLI, Qwen Code, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi Coding Agent, Oh My Pi, Forge, Goose, Mistral Vibe, or ZCode. If you don't have the required tool installed, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
|
||||
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, Forge, Goose, Mistral Vibe, or ZCode 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> --integration copilot --ignore-agent-tools
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# Community Bundles
|
||||
|
||||
> [!NOTE]
|
||||
> Community bundles are independently created and maintained by their respective authors. Maintainers only verify that submission metadata is complete and correctly formatted — they do **not review, audit, endorse, or support the bundle code or the components it installs**. Review bundle manifests, component catalogs, and source repositories before installation and use at your own discretion.
|
||||
|
||||
Bundles compose existing Spec Kit components — extensions, presets, workflows, and steps — into a single role or team stack. They are useful when a user should be able to install a tested set of components together instead of following several separate install commands.
|
||||
|
||||
Accepted community bundle entries will be listed here once a community bundle catalog is available. To submit a bundle for review, file a [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue.
|
||||
|
||||
## What to Submit
|
||||
|
||||
A bundle submission should include:
|
||||
|
||||
- A public repository with a valid `bundle.yml` manifest.
|
||||
- A versioned GitHub release with a bundle artifact created by `specify bundle build`.
|
||||
- Documentation that explains the intended role, installed components, required catalogs, and expected workflow.
|
||||
- A proposed catalog entry with bundle metadata and component counts.
|
||||
- Test evidence from a clean Spec Kit project.
|
||||
|
||||
## Component Resolution
|
||||
|
||||
A bundle catalog entry describes where to download the bundle artifact, but the bundle's component references still need to resolve when a user installs it. References can resolve from bundled components, already installed components, or active extension, preset, workflow, and step catalogs.
|
||||
|
||||
If your bundle depends on components that are not available from the default Spec Kit catalogs, include the required catalog URLs in the submission and in your README. Test the full install path from a clean project with those catalogs added before submitting.
|
||||
|
||||
For example:
|
||||
|
||||
```bash
|
||||
specify preset catalog add https://example.com/presets.json --name example-bundle --install-allowed
|
||||
specify extension catalog add https://example.com/extensions.json --name example-bundle --install-allowed
|
||||
curl -L -o example-bundle-1.0.0.zip https://example.com/example-bundle-1.0.0.zip
|
||||
specify bundle install ./example-bundle-1.0.0.zip
|
||||
|
||||
# Or install by id from an install-allowed bundle catalog.
|
||||
specify bundle catalog add https://example.com/bundles.json --id example-bundle-catalog --policy install-allowed
|
||||
specify bundle install example-bundle
|
||||
```
|
||||
|
||||
## Review Scope
|
||||
|
||||
Maintainers check that:
|
||||
|
||||
- The submission fields are complete and correctly formatted.
|
||||
- The release artifact and documentation URLs are reachable.
|
||||
- The repository contains a `bundle.yml` manifest.
|
||||
- The submission clearly identifies any required component catalogs.
|
||||
- The proposed catalog entry uses the expected bundle catalog entry shape.
|
||||
|
||||
Maintainers do not audit the behavior of installed extensions, presets, workflows, steps, or scripts. Users should review those components before installing a community bundle.
|
||||
|
||||
## Updating a Bundle
|
||||
|
||||
To update a submitted bundle, file another [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue with the new version, download URL, changed component list, and updated test evidence. Mention that the issue updates an existing bundle entry.
|
||||
@@ -31,7 +31,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| API Evolve | Managed API contract evolution — breaking-change detection, semver enforcement, deprecation orchestration, and lifecycle gates across REST, GraphQL, and gRPC | `process` | Read+Write | [spec-kit-api-evolve](https://github.com/Quratulain-bilal/spec-kit-api-evolve) |
|
||||
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
|
||||
| Architecture Guard | Framework-agnostic architecture review extension for validating implementation against governance and architecture constitutions, detecting architectural drift, and generating non-blocking refactor tasks | `process` | Read+Write | [spec-kit-architecture-guard](https://github.com/DyanGalih/spec-kit-architecture-guard) |
|
||||
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views with per-view and full-workflow commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
|
||||
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views as separate commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
|
||||
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
|
||||
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
|
||||
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
|
||||
@@ -56,9 +56,8 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Fleet Orchestrator | Orchestrate a full feature lifecycle with human-in-the-loop gates across all SpecKit phases | `process` | Read+Write | [spec-kit-fleet](https://github.com/sharathsatish/spec-kit-fleet) |
|
||||
| GitHub Issues Integration 1 | Generate spec artifacts from GitHub Issues - import issues, sync updates, and maintain bidirectional traceability | `integration` | Read+Write | [spec-kit-github-issues](https://github.com/Fatima367/spec-kit-github-issues) |
|
||||
| GitHub Issues Integration 2 | Creates and syncs local specs from an existing GitHub issue | `integration` | Read+Write | [spec-kit-issue](https://github.com/aaronrsun/spec-kit-issue) |
|
||||
| Golden Demo | Extracts acceptance criteria from specs, builds test vectors, and produces a behavioral drift report — complementary to Architecture Guard and CDD | `docs` | Read+Write | [spec-kit-golden-demo](https://github.com/jasstt/spec-kit-golden-demo) |
|
||||
| Improve Extension | Audits any codebase as a senior advisor and writes prioritized, self-contained spec prompts under specs/ that the spec-kit lifecycle can process | `process` | Read+Write | [spec-kit-improve](https://github.com/d0whc3r/spec-kit-improve) |
|
||||
| Intake | Normalize PRD, design, HTML SSOT, and test-case evidence into SDD-ready intake artifacts. | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) |
|
||||
| Intake | Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts | `docs` | Read+Write | [spec-kit-intake](https://github.com/bigsmartben/spec-kit-intake) |
|
||||
| Intelligent Agent Orchestrator | Cross-catalog agent discovery and intelligent prompt-to-command routing | `process` | Read+Write | [spec-kit-orchestrator](https://github.com/pragya247/spec-kit-orchestrator) |
|
||||
| Iterate | Iterate on spec documents with a two-phase define-and-apply workflow — refine specs mid-implementation and go straight back to building | `docs` | Read+Write | [spec-kit-iterate](https://github.com/imviancagrace/spec-kit-iterate) |
|
||||
| Jira Integration | Create Jira Epics, Stories, and Issues from spec-kit specifications and task breakdowns with configurable hierarchy and custom field support | `integration` | Read+Write | [spec-kit-jira](https://github.com/mbachorik/spec-kit-jira) |
|
||||
@@ -98,7 +97,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
|
||||
| Red Team | Adversarial review of specs before /speckit.plan — parallel lens agents surface risks that clarify/analyze structurally can't (prompt injection, integrity gaps, cross-spec drift, silent failures). Produces a structured findings report; no auto-edits to specs. | `docs` | Read+Write | [spec-kit-red-team](https://github.com/ashbrener/spec-kit-red-team) |
|
||||
| Research Harness | State-externalizing research harness: budgeted exploration, evidence curation, and claim verification for spec-driven development | `process` | Read+Write | [spec-kit-harness](https://github.com/formin/spec-kit-harness) |
|
||||
| Repository Governance | Generate project-governance projections from Spec Kit metadata | `process` | Read+Write | [spec-kit-agent-governance](https://github.com/bigsmartben/spec-kit-agent-governance) |
|
||||
| Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) |
|
||||
| Reqnroll BDD | Adds Reqnroll BDD planning, Gherkin generation, traceability, safe task injection, handoff, and verification to Spec Kit | `process` | Read+Write | [spec-kit-reqnroll-bdd](https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd) |
|
||||
| Retro Extension | Sprint retrospective analysis with metrics, spec accuracy assessment, and improvement suggestions | `process` | Read+Write | [spec-kit-retro](https://github.com/arunt14/spec-kit-retro) |
|
||||
@@ -119,7 +117,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Spec Orchestrator | Cross-feature orchestration — track state, select tasks, and detect conflicts across parallel specs | `process` | Read-only | [spec-kit-orchestrator](https://github.com/Quratulain-bilal/spec-kit-orchestrator) |
|
||||
| Spec Reference Loader | Reads the ## References section from the feature spec and loads only the listed docs into context | `docs` | Read-only | [spec-kit-spec-reference-loader](https://github.com/KevinBrown5280/spec-kit-spec-reference-loader) |
|
||||
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
|
||||
| Spec Roadmap | Capture a durable spec roadmap after the constitution, then review specs against it before and after implementation so spec-specific decisions, outcomes, and constraints are never lost. | `process` | Read+Write | [speckit-roadmap](https://github.com/srobroek/speckit-roadmap) |
|
||||
| Spec Scope | Effort estimation and scope tracking — estimate work, detect creep, and budget time per phase | `process` | Read-only | [spec-kit-scope-](https://github.com/Quratulain-bilal/spec-kit-scope-) |
|
||||
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
|
||||
| Spec Trace | Build a requirement → test traceability matrix from spec.md and the test suite — surface untested requirements and orphan tests | `code` | Read+Write | [spec-kit-trace](https://github.com/Quratulain-bilal/spec-kit-trace) |
|
||||
|
||||
@@ -7,9 +7,7 @@ Community projects that extend, visualize, or build on Spec Kit:
|
||||
|
||||
- **[cc-spex](https://github.com/rhuss/cc-spex)** — A Claude Code plugin that adds composable traits on top of Spec Kit with [Superpowers](https://github.com/obra/superpowers)-based quality gates, spec/code review, git worktree isolation, and parallel implementation via agent teams.
|
||||
|
||||
- **[VS Code Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
||||
|
||||
- **[SpecKit Assistant](https://www.npmjs.com/package/speckit-assistant)** — A visual orchestrator for Spec-Driven Development (SDD). It connects your local specification, planning, and task checklists with AI agents (Claude, Gemini, GitHub Copilot). No global installation required — just run it via `npx speckit-assistant`.
|
||||
- **[Spec Kit Assistant](https://marketplace.visualstudio.com/items?itemName=rfsales.speckit-assistant)** — A VS Code extension that provides a visual orchestrator for the full SDD workflow (constitution → specification → planning → tasks → implementation) with phase status visualization, an interactive task checklist, DAG visualization, and support for Claude, Gemini, GitHub Copilot, and OpenAI backends. Requires the `specify` CLI in your PATH.
|
||||
|
||||
- **[SpecKit Companion](https://marketplace.visualstudio.com/items?itemName=alfredoperez.speckit-companion)** — A VS Code extension that brings a visual GUI to Spec Kit. Browse specs in a rich markdown viewer with clickable file references, create specifications with image attachments, comment and refine each step inline (GitHub-style review), track your progress through the SDD workflow with a visual phase stepper, and manage steering documents like constitutions and templates.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Community
|
||||
|
||||
The Spec Kit community builds extensions, presets, bundles, walkthroughs, and companion projects that expand what you can do with Spec-Driven Development. All community contributions are independently created and maintained by their respective authors.
|
||||
The Spec Kit community builds extensions, presets, walkthroughs, and companion projects that expand what you can do with Spec-Driven Development. All community contributions are independently created and maintained by their respective authors.
|
||||
|
||||
## Extensions
|
||||
|
||||
@@ -14,12 +14,6 @@ Presets customize how Spec Kit behaves — overriding templates, commands, and t
|
||||
|
||||
[Browse community presets →](presets.md)
|
||||
|
||||
## Bundles
|
||||
|
||||
Bundles compose extensions, presets, workflows, and steps into role or team stacks that can be installed together.
|
||||
|
||||
[Browse community bundles →](bundles.md)
|
||||
|
||||
## Walkthroughs
|
||||
|
||||
Step-by-step guides that show Spec-Driven Development in action across different scenarios, languages, and frameworks.
|
||||
|
||||
@@ -25,7 +25,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
|
||||
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
|
||||
| Screenwriting | Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Spec Kit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks. Export to Fountain, FTX, PDF | 26 templates, 32 commands, 1 script | — | [speckit-preset-screenwriting](https://github.com/adaumann/speckit-preset-screenwriting) |
|
||||
| Security Governance | Adds memory-safe-language preference, language-specific secure coding profiles, audit-ready Spec-Kit run evidence, ASVS verification, SBOM/AI-SBOM supply-chain transparency, CRA awareness, and regulatory applicability screening for NIS2, CRA, EU AI Act, and DORA | 14 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
|
||||
| SicarioSpec Core | Baseline secure-by-default Spec Kit governance profile. | 5 templates | — | [sicario-spec](https://github.com/dfirs1car1o/sicario-spec) |
|
||||
| SicarioSpec Core | Evidence-first security operations governance that maps feature risk to controls, gates, evidence, owners, approval, and accepted-risk decisions. | 5 templates | — | [sicario-spec](https://github.com/dfirs1car1o/sicario-spec) |
|
||||
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy | 5 templates, 8 commands | — | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
|
||||
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
|
||||
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
|
||||
|
||||
@@ -26,7 +26,6 @@ through the standard flow:
|
||||
2. Run `/speckit.plan` to define the implementation approach.
|
||||
3. Run `/speckit.tasks` to derive the work breakdown.
|
||||
4. Run `/speckit.implement` and review the resulting code and artifact diffs.
|
||||
5. Run `/speckit.converge` to verify completeness and generate tasks for remaining gaps. If tasks are appended, repeat `/speckit.implement` and `/speckit.converge` until the feature is fully complete.
|
||||
|
||||
The previous feature directory remains intact for audit, comparison, or
|
||||
explaining how the project reached its current state. Use clear feature names or
|
||||
@@ -51,7 +50,6 @@ spec:
|
||||
5. Run `/speckit.analyze` before implementation resumes to catch gaps between
|
||||
the spec, plan, and tasks.
|
||||
6. Run `/speckit.implement`, then review the code and artifact diffs together.
|
||||
7. Run `/speckit.converge` to assess completion and append any remaining work to `tasks.md`. If tasks are appended, repeat `/speckit.implement` and `/speckit.converge` until the feature is fully complete.
|
||||
|
||||
Preserve important implementation rationale before replacing derived artifacts.
|
||||
If a plan or task list contains decisions that still matter, carry them forward
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
# Using Spec Kit in a Monorepo
|
||||
|
||||
A Spec Kit project is **directory-scoped**: the project is whichever directory
|
||||
contains `.specify/`. A monorepo can hold several independent Spec Kit projects
|
||||
under one repository root, each with its own `.specify/`, `specs/`, constitution,
|
||||
and feature numbering.
|
||||
|
||||
Root resolution already prefers the **nearest** `.specify/` over the Git
|
||||
toplevel, so commands run from inside a member project resolve to that project,
|
||||
not the repo root.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
my-monorepo/
|
||||
├── .git/ # one Git repository at the root
|
||||
├── apps/
|
||||
│ ├── web/
|
||||
│ │ └── .specify/ # Spec Kit project "web"
|
||||
│ │ └── memory/constitution.md
|
||||
│ └── api/
|
||||
│ └── .specify/ # Spec Kit project "api"
|
||||
│ └── memory/constitution.md
|
||||
└── packages/
|
||||
└── ui/
|
||||
└── .specify/ # Spec Kit project "ui"
|
||||
```
|
||||
|
||||
Initialize each member project independently:
|
||||
|
||||
```bash
|
||||
specify init apps/web --integration claude
|
||||
specify init apps/api --integration claude
|
||||
```
|
||||
|
||||
Each project keeps its own `specs/` directory and numbers features
|
||||
independently (`apps/web/specs/001-…`, `apps/api/specs/001-…`).
|
||||
|
||||
## Working inside a member project
|
||||
|
||||
The default workflow is unchanged: change into the project directory and run the
|
||||
slash commands. Root resolution finds the nearest `.specify/`.
|
||||
|
||||
```bash
|
||||
cd apps/web
|
||||
# then run /speckit.specify, /speckit.plan, … in your agent
|
||||
```
|
||||
|
||||
## Targeting a member project from the repo root
|
||||
|
||||
For non-interactive or CI runs where you do not want to `cd`, set
|
||||
**`SPECIFY_INIT_DIR`** to the member project root (the directory *containing*
|
||||
`.specify/`). Relative paths resolve against the current directory.
|
||||
|
||||
```bash
|
||||
# operate on apps/web from the monorepo root (no cd required)
|
||||
export SPECIFY_INIT_DIR=apps/web
|
||||
```
|
||||
|
||||
The path must exist and contain `.specify/`. If it does not, the command
|
||||
**errors and does not fall back** to the current directory or the Git toplevel.
|
||||
This is deliberate: a typo never writes specs into the wrong project. A
|
||||
nonexistent path is reported as you typed it; a path that exists but is not a
|
||||
Spec Kit project is reported as its resolved absolute path:
|
||||
|
||||
```text
|
||||
# SPECIFY_INIT_DIR=apps/wbe (typo: no such directory)
|
||||
ERROR: SPECIFY_INIT_DIR does not point to an existing directory: apps/wbe
|
||||
|
||||
# SPECIFY_INIT_DIR=apps (exists, but has no .specify/ of its own)
|
||||
ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): /home/you/my-monorepo/apps
|
||||
```
|
||||
|
||||
`SPECIFY_INIT_DIR` selects the **project**; `SPECIFY_FEATURE_DIRECTORY` selects
|
||||
the **feature** within it. They compose: set both to pick a project and a
|
||||
feature non-interactively. See the
|
||||
[`SPECIFY_INIT_DIR` reference](../reference/core.md#environment-variables) for
|
||||
the full contract and the two-axes model.
|
||||
|
||||
## How `SPECIFY_INIT_DIR` reaches your agent
|
||||
|
||||
`SPECIFY_INIT_DIR` is read by the shell scripts that the slash commands invoke
|
||||
(`get_repo_root` in Bash, `Get-RepoRoot` in PowerShell). It takes effect only
|
||||
when it is present in the environment of the shell that runs those scripts.
|
||||
|
||||
- **Scripted / CI runs:** export it in the same shell that drives the commands;
|
||||
it is reliable there.
|
||||
- **Interactive agents:** whether an exported variable reaches the shell tool an
|
||||
agent uses is agent-specific. Export `SPECIFY_INIT_DIR` *before* launching the
|
||||
agent, and verify once (e.g. run `/speckit.specify` and confirm the new feature
|
||||
landed under the intended project's `specs/`).
|
||||
|
||||
## Git in a monorepo
|
||||
|
||||
> [!NOTE]
|
||||
> Spec Kit project files are scoped to the **resolved project root**, but Git
|
||||
> operations still run in the containing Git work tree. In a monorepo with a
|
||||
> single Git repository at the root and projects in subdirectories, feature
|
||||
> branch creation creates or switches branches in the shared root repository.
|
||||
> Spec directories still live under the selected member project, while the Git
|
||||
> branch namespace is shared by the whole monorepo. Manage branches and commits
|
||||
> at the repository root, or initialize Git per member project if you want
|
||||
> isolated per-project branch namespaces.
|
||||
|
||||
## Constitutions
|
||||
|
||||
Each member project has its own `.specify/memory/constitution.md` and
|
||||
`/speckit.constitution` edits the local project's file. Spec Kit does not provide
|
||||
a built-in base/inheritance mechanism; if you want one constitution to reference
|
||||
shared rules elsewhere in the monorepo, you need to maintain that wiring yourself.
|
||||
Otherwise, duplicate or sync shared engineering rules per project.
|
||||
@@ -31,7 +31,7 @@ Define what to build before building it. Rich templates, quality checklists, and
|
||||
|
||||
### Use any coding agent
|
||||
|
||||
<span class="pillar-stat">30+ integrations</span> — Copilot, Gemini, Codex, Kilo Code, Zed, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in.
|
||||
<span class="pillar-stat">30+ integrations</span> — Copilot, Gemini, Codex, Windsurf, Zed, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in.
|
||||
|
||||
Run `specify init` with your agent of choice and Spec Kit sets up the right command files, context rules, and directory structures automatically. If your agent isn't listed, the `generic` integration is an escape hatch for any tool.
|
||||
|
||||
|
||||
@@ -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.cn/docs/cli/installation), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Pi Coding Agent](https://pi.dev), or [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent)
|
||||
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Codebuddy CLI](https://www.codebuddy.ai/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Pi Coding Agent](https://pi.dev)
|
||||
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pipx.pypa.io/) for persistent installation
|
||||
- [Python 3.11+](https://www.python.org/downloads/)
|
||||
- [Git](https://git-scm.com/downloads) _(optional — required only when the git extension is enabled)_
|
||||
@@ -51,7 +51,6 @@ specify init <project_name> --integration gemini
|
||||
specify init <project_name> --integration copilot
|
||||
specify init <project_name> --integration codebuddy
|
||||
specify init <project_name> --integration pi
|
||||
specify init <project_name> --integration omp
|
||||
```
|
||||
|
||||
### Specify Script Type (Shell vs PowerShell)
|
||||
@@ -94,15 +93,8 @@ This helps verify you are running the official Spec Kit build from GitHub, not a
|
||||
After initialization, you should see the following commands available in your coding agent:
|
||||
|
||||
- `/speckit.specify` - Create specifications
|
||||
- `/speckit.plan` - Generate implementation plans
|
||||
- `/speckit.plan` - Generate implementation plans
|
||||
- `/speckit.tasks` - Break down into actionable tasks
|
||||
- `/speckit.implement` - Execute implementation tasks
|
||||
- `/speckit.analyze` - Validate cross-artifact consistency
|
||||
- `/speckit.clarify` - Identify and resolve ambiguities
|
||||
- `/speckit.checklist` - Generate quality checklists
|
||||
- `/speckit.constitution` - Create or update project principles
|
||||
- `/speckit.converge` - Assess codebase against artifacts and append remaining tasks
|
||||
- `/speckit.taskstoissues` - Convert tasks to issues
|
||||
|
||||
Scripts are installed into a variant subdirectory matching the chosen script type:
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ This guide will help you get started with Spec-Driven Development using Spec Kit
|
||||
After installing Spec Kit and defining your project constitution, quick experiments can use the lean feature path: `/speckit.specify` -> `/speckit.plan` -> `/speckit.tasks` -> `/speckit.implement`. For production features or any work with meaningful ambiguity, treat `/speckit.clarify`, `/speckit.checklist`, and `/speckit.analyze` as regular quality gates:
|
||||
|
||||
```text
|
||||
/speckit.constitution -> /speckit.specify -> /speckit.clarify -> /speckit.plan -> /speckit.checklist -> /speckit.tasks -> /speckit.analyze -> /speckit.implement -> /speckit.converge
|
||||
/speckit.constitution -> /speckit.specify -> /speckit.clarify -> /speckit.checklist -> /speckit.plan -> /speckit.tasks -> /speckit.analyze -> /speckit.implement
|
||||
```
|
||||
|
||||
Use `/speckit.clarify` to reduce requirement ambiguity before planning, `/speckit.checklist` (after `/speckit.plan`) to generate quality checklists that validate requirements completeness, clarity, and consistency, and `/speckit.analyze` to check spec/plan/task consistency before implementation starts. You can repeat `/speckit.analyze` after implementation as an extra review, but keep the first analysis before `/speckit.implement` so gaps are caught while the plan and tasks can still be adjusted. Finally, run `/speckit.converge` after implementation to verify all planned work is complete and generate tasks for any remaining gaps. If `/speckit.converge` appends new tasks, run `/speckit.implement` again (and converge again) until it reports that the feature has converged.
|
||||
Use `/speckit.clarify` to reduce requirement ambiguity before planning, `/speckit.checklist` to validate requirements quality before planning, and `/speckit.analyze` to check spec/plan/task consistency before implementation starts. You can repeat `/speckit.analyze` after implementation as an extra review, but keep the first analysis before `/speckit.implement` so gaps are caught while the plan and tasks can still be adjusted.
|
||||
|
||||
### Step 1: Install Specify
|
||||
|
||||
@@ -75,6 +75,12 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
|
||||
/speckit.clarify Focus on security and performance requirements.
|
||||
```
|
||||
|
||||
Then validate the requirements with `/speckit.checklist` before creating the technical plan:
|
||||
|
||||
```bash
|
||||
/speckit.checklist
|
||||
```
|
||||
|
||||
### Step 5: Create a Technical Implementation Plan
|
||||
|
||||
**In the chat**, use the `/speckit.plan` slash command to provide your tech stack and architecture choices.
|
||||
@@ -83,12 +89,6 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
|
||||
/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.
|
||||
```
|
||||
|
||||
Then generate quality checklists with `/speckit.checklist` once the plan exists:
|
||||
|
||||
```bash
|
||||
/speckit.checklist
|
||||
```
|
||||
|
||||
### Step 6: Break Down, Analyze, and Implement
|
||||
|
||||
**In the chat**, use the `/speckit.tasks` slash command to create an actionable task list.
|
||||
@@ -150,7 +150,15 @@ You can continue to refine the spec with more details using `/speckit.clarify`:
|
||||
/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.
|
||||
```
|
||||
|
||||
### Step 4: Generate Technical Plan with `/speckit.plan`
|
||||
### 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`
|
||||
|
||||
Be specific about your tech stack and technical requirements:
|
||||
|
||||
@@ -158,14 +166,6 @@ Be specific about your tech stack and technical requirements:
|
||||
/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.
|
||||
```
|
||||
|
||||
### Step 5: Validate the Spec
|
||||
|
||||
Generate quality checklists to validate the specification using the `/speckit.checklist` command:
|
||||
|
||||
```bash
|
||||
/speckit.checklist
|
||||
```
|
||||
|
||||
### Step 6: Define Tasks
|
||||
|
||||
Generate an actionable task list using the `/speckit.tasks` command:
|
||||
@@ -188,14 +188,6 @@ Finally, implement the solution:
|
||||
/speckit.implement
|
||||
```
|
||||
|
||||
### Step 8: Converge
|
||||
|
||||
Run the `/speckit.converge` command after implementation to assess the current codebase against the feature's artifacts and append any remaining unbuilt work as new tasks to `tasks.md`. If the command appends new tasks, run `/speckit.implement` again to complete them, and repeat the converge step until the feature is fully complete.
|
||||
|
||||
```bash
|
||||
/speckit.converge
|
||||
```
|
||||
|
||||
> [!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.
|
||||
|
||||
|
||||
@@ -69,33 +69,6 @@ Either `token` or `token_env` must be set for `bearer` and `basic-pat` schemes.
|
||||
}
|
||||
```
|
||||
|
||||
### GitHub Enterprise Server (GHES)
|
||||
|
||||
To use a private catalog or extension hosted on a GitHub Enterprise Server
|
||||
instance, add a `github` entry listing your GHES host(s). The same entry
|
||||
authenticates both catalog JSON fetches **and** private release-asset
|
||||
downloads — Specify recognizes the listed hosts as GitHub Enterprise and
|
||||
resolves release downloads through the GHES REST API (`/api/v3`).
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": [
|
||||
{
|
||||
"hosts": ["ghes.example.com", "raw.ghes.example.com", "codeload.ghes.example.com"],
|
||||
"provider": "github",
|
||||
"auth": "bearer",
|
||||
"token_env": "GH_ENTERPRISE_TOKEN"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
List the **bare** web host (e.g. `ghes.example.com`) — release-download URLs
|
||||
live there. If your instance uses subdomain isolation, also list the `raw.`
|
||||
and `codeload.` subdomains your catalog/extension URLs use. A
|
||||
`*.ghes.example.com` wildcard matches subdomains but **not** the bare host,
|
||||
so always include the bare host explicitly.
|
||||
|
||||
### Azure DevOps (`azure-devops`)
|
||||
|
||||
| Scheme | Header | Use for |
|
||||
|
||||
@@ -119,12 +119,6 @@ specify bundle build
|
||||
|
||||
Produces a single versioned, distributable `.zip` artifact from a bundle directory. The artifact embeds the manifest and can be installed directly with `specify bundle install <artifact.zip>`.
|
||||
|
||||
## Publish a Bundle
|
||||
|
||||
Bundle authors validate and package bundles locally, then host the generated artifact and catalog metadata where users can access it. A bundle catalog entry points at the bundle artifact, but the components declared inside `bundle.yml` still resolve through bundled components, installed components, or active extension, preset, workflow, and step catalogs.
|
||||
|
||||
If your bundle references components from non-default catalogs, document those catalog URLs and test the install path from a clean project with those catalogs added. Community bundle submissions should include that dependency-resolution evidence in the [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue.
|
||||
|
||||
## Manage Catalog Sources
|
||||
|
||||
Bundles are discovered through a priority-ordered stack of catalog sources (project, user, and built-in scopes).
|
||||
|
||||
@@ -26,7 +26,6 @@ specify extension add <name>
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| `--dev` | Install from a local directory (for development) |
|
||||
| `--from <url>` | Install from a custom URL instead of the catalog |
|
||||
| `--force` | Overwrite if already installed |
|
||||
| `--priority <N>`| Resolution priority (default: 10; lower = higher precedence) |
|
||||
|
||||
Installs an extension from the catalog, a URL, or a local directory. Extension commands are automatically registered with the currently installed AI coding agent integration.
|
||||
|
||||
@@ -11,7 +11,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | `auggie` | |
|
||||
| [Claude Code](https://www.anthropic.com/claude-code) | `claude` | Skills-based integration; installs skills in `.claude/skills` |
|
||||
| [Cline](https://github.com/cline/cline) | `cline` | IDE-based agent |
|
||||
| [CodeBuddy CLI](https://www.codebuddy.cn/docs/cli/installation) | `codebuddy` | |
|
||||
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | `codebuddy` | |
|
||||
| [Codex CLI](https://github.com/openai/codex) | `codex` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `$speckit-<command>` |
|
||||
| [Cursor](https://cursor.sh/) | `cursor-agent` | |
|
||||
| [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-<command>` |
|
||||
@@ -19,24 +19,26 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
| [Forge](https://forgecode.dev/) | `forge` | |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
|
||||
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | |
|
||||
| [Goose](https://goose-docs.ai/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
|
||||
| [Goose](https://block.github.io/goose/) | `goose` | Uses YAML recipe format in `.goose/recipes/` |
|
||||
| [Hermes](https://github.com/NousResearch/hermes-agent) | `hermes` | Skills-based integration; installs skills globally into `~/.hermes/skills/` |
|
||||
| [IBM Bob](https://www.ibm.com/products/bob) | `bob` | IDE-based agent |
|
||||
| [iFlow CLI](https://docs.iflow.cn/en/cli/quickstart) | `iflow` | |
|
||||
| [Junie](https://junie.jetbrains.com/) | `junie` | |
|
||||
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | `kilocode` | |
|
||||
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; installs into `.kimi-code/skills/`. `--migrate-legacy` moves old `.kimi/skills/` installs to the new paths, and (when the `agent-context` extension is enabled) migrates `KIMI.md` context into `AGENTS.md` |
|
||||
| [Kimi Code](https://code.kimi.com/) | `kimi` | Skills-based integration; supports `--migrate-legacy` for dotted→hyphenated directory migration |
|
||||
| [Kiro CLI](https://kiro.dev/docs/cli/) | `kiro-cli` | Kiro CLI does not substitute `$ARGUMENTS` in file-based prompts, so Spec Kit ships a prose fallback at render time (see [Manage prompts](https://kiro.dev/docs/cli/chat/manage-prompts/) and issue [#1926](https://github.com/github/spec-kit/issues/1926)). Alias: `--integration kiro` |
|
||||
| [Lingma](https://lingma.aliyun.com/) | `lingma` | Skills-based integration; skills are installed automatically |
|
||||
| [Mistral Vibe](https://github.com/mistralai/mistral-vibe) | `vibe` | |
|
||||
| [Oh My Pi](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent) | `omp` | Installs slash commands into `.omp/commands` |
|
||||
| [opencode](https://opencode.ai/) | `opencode` | |
|
||||
| [Pi Coding Agent](https://pi.dev) | `pi` | Pi doesn't have MCP support out of the box, so `taskstoissues` won't work as intended. MCP support can be added via [extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent#extensions) |
|
||||
| [Qoder CLI](https://qoder.com/cli) | `qodercli` | |
|
||||
| [Qwen Code](https://github.com/QwenLM/qwen-code) | `qwen` | |
|
||||
| [Roo Code](https://roocode.com/) | `roo` | |
|
||||
| [RovoDev](https://www.atlassian.com/software/rovo-dev) | `rovodev` | Generates `.rovodev/skills/`, prompt wrappers, and `prompts.yml`; runtime dispatch uses `acli rovodev` |
|
||||
| [SHAI (OVHcloud)](https://github.com/ovh/shai) | `shai` | |
|
||||
| [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | `tabnine` | |
|
||||
| [Trae](https://www.trae.ai/) | `trae` | Skills-based integration; skills are installed automatically |
|
||||
| [Windsurf](https://windsurf.com/) | `windsurf` | |
|
||||
| [ZCode](https://zcode.z.ai/) | `zcode` | Skills-based integration; installs skills into `.zcode/skills/` and invokes them as `$speckit-<command>` |
|
||||
| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-<command>` |
|
||||
| Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir <path>"` for AI coding agents not listed above |
|
||||
@@ -51,27 +53,6 @@ Shows all available integrations, which one is currently installed, and whether
|
||||
When multiple integrations are installed, the list marks the default integration separately from the other installed integrations.
|
||||
The list also shows whether each built-in integration is declared multi-install safe.
|
||||
|
||||
## Search Available Integrations
|
||||
|
||||
```bash
|
||||
specify integration search [query]
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| ---------- | ------------------ |
|
||||
| `--tag` | Filter by tag |
|
||||
| `--author` | Filter by author |
|
||||
|
||||
Searches the active catalog stack for integrations matching the query. Without a query, lists all available integrations. Must be run inside a Spec Kit project.
|
||||
|
||||
## Integration Info
|
||||
|
||||
```bash
|
||||
specify integration info <integration_id>
|
||||
```
|
||||
|
||||
Shows catalog details for a single integration, including its description, author, license, tags, source catalog, repository (when available), and whether it is currently active. Must be run inside a Spec Kit project.
|
||||
|
||||
## Install an Integration
|
||||
|
||||
```bash
|
||||
@@ -118,7 +99,6 @@ specify integration switch <key>
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||
| `--force` | Force removal of modified files during uninstall; when the target is already installed, overwrite managed shared templates while changing the default |
|
||||
| `--refresh-shared-infra` | Also overwrite shared infrastructure files even if you customized them (otherwise customizations are preserved) |
|
||||
| `--integration-options` | Options for the target integration when it is not already installed |
|
||||
|
||||
If the target integration is not already installed, equivalent to running `uninstall` followed by `install` in a single step. In this mode, `--force` controls whether modified files from the removed integration are deleted. If the target integration is already installed, `switch` only changes the default integration, like `use`; in this mode, `--force` controls whether managed shared templates are overwritten while the default changes. `--integration-options` is rejected for already-installed targets because changing integration options requires reinstalling managed files; run `upgrade <key> --integration-options ...` first, then `use <key>`.
|
||||
@@ -170,47 +150,6 @@ is `null` when no installed integration set can be evaluated, such as when the
|
||||
integration state is missing, unreadable, lacks a valid recorded integration
|
||||
list, or records no installed integrations.
|
||||
|
||||
## Catalog Management
|
||||
|
||||
Integration catalogs control where the discovery commands (`search` and `info`) look for integrations. Catalogs are checked in priority order.
|
||||
|
||||
### List Catalogs
|
||||
|
||||
```bash
|
||||
specify integration catalog list
|
||||
```
|
||||
|
||||
Shows the active catalog sources. Project-level sources (when configured) are removable by index; otherwise the active sources are shown as non-removable.
|
||||
|
||||
### Add a Catalog
|
||||
|
||||
```bash
|
||||
specify integration catalog add <url>
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
| --------------- | ----------------------------- |
|
||||
| `--name <name>` | Optional name for the catalog |
|
||||
|
||||
Adds a custom catalog URL to the project's `.specify/integration-catalogs.yml`. The URL must use HTTPS (except `http://localhost`, `http://127.0.0.1`, or `http://[::1]` for local testing).
|
||||
|
||||
### Remove a Catalog
|
||||
|
||||
```bash
|
||||
specify integration catalog remove <index>
|
||||
```
|
||||
|
||||
Removes a project catalog source by its 0-based index in `catalog list`.
|
||||
|
||||
### Catalog Resolution Order
|
||||
|
||||
Catalogs are resolved in this order (first match wins):
|
||||
|
||||
1. **Environment variable** — `SPECKIT_INTEGRATION_CATALOG_URL` overrides all catalogs
|
||||
2. **Project config** — `.specify/integration-catalogs.yml`
|
||||
3. **User config** — `~/.specify/integration-catalogs.yml`
|
||||
4. **Built-in defaults** — official catalog + community catalog
|
||||
|
||||
## Integration-Specific Options
|
||||
|
||||
Some integrations accept additional options via `--integration-options`:
|
||||
@@ -218,7 +157,7 @@ Some integrations accept additional options via `--integration-options`:
|
||||
| Integration | Option | Description |
|
||||
| ----------- | ------------------- | -------------------------------------------------------------- |
|
||||
| `generic` | `--commands-dir` | Required. Directory for command files |
|
||||
| `kimi` | `--migrate-legacy` | Migrate legacy `.kimi/skills/` installs to `.kimi-code/skills/` (including dotted→hyphenated directory names); when the `agent-context` extension is enabled, also migrates `KIMI.md` to `AGENTS.md` |
|
||||
| `kimi` | `--migrate-legacy` | Migrate legacy dotted skill directories to hyphenated format |
|
||||
|
||||
Example:
|
||||
|
||||
@@ -226,18 +165,6 @@ Example:
|
||||
specify integration install generic --integration-options="--commands-dir .myagent/cmds"
|
||||
```
|
||||
|
||||
## Scaffold a New Integration
|
||||
|
||||
```bash
|
||||
specify integration scaffold <key>
|
||||
```
|
||||
|
||||
Creates a minimal built-in integration package and a matching test skeleton in the Spec Kit repository, then prints the next steps for wiring it up. Run this command from the Spec Kit repository root. The `<key>` must be lowercase kebab-case (for example, `my-agent`).
|
||||
|
||||
| Option | Description |
|
||||
| -------- | ---------------------------------------------------------------- |
|
||||
| `--type` | Scaffold template to use: `markdown` (default), `skills`, `toml`, or `yaml` |
|
||||
|
||||
## FAQ
|
||||
|
||||
### Can I install multiple integrations in the same project?
|
||||
@@ -256,20 +183,22 @@ The currently declared multi-install safe integrations are:
|
||||
| --- | --------- |
|
||||
| `auggie` | `.augment/commands`, `.augment/rules/specify-rules.md` |
|
||||
| `claude` | `.claude/skills`, `CLAUDE.md` |
|
||||
| `cline` | `.clinerules/workflows`, `.clinerules/specify-rules.md` |
|
||||
| `codebuddy` | `.codebuddy/commands`, `CODEBUDDY.md` |
|
||||
| `codex` | `.agents/skills`, `AGENTS.md` |
|
||||
| `cursor-agent` | `.cursor/skills`, `.cursor/rules/specify-rules.mdc` |
|
||||
| `firebender` | `.firebender/commands`, `.firebender/rules/specify-rules.mdc` |
|
||||
| `gemini` | `.gemini/commands`, `GEMINI.md` |
|
||||
| `iflow` | `.iflow/commands`, `IFLOW.md` |
|
||||
| `junie` | `.junie/commands`, `.junie/AGENTS.md` |
|
||||
| `kilocode` | `.kilocode/workflows`, `.kilocode/rules/specify-rules.md` |
|
||||
| `kimi` | `.kimi/skills`, `KIMI.md` |
|
||||
| `qodercli` | `.qoder/commands`, `QODER.md` |
|
||||
| `qwen` | `.qwen/commands`, `QWEN.md` |
|
||||
| `roo` | `.roo/commands`, `.roo/rules/specify-rules.md` |
|
||||
| `shai` | `.shai/commands`, `SHAI.md` |
|
||||
| `tabnine` | `.tabnine/agent/commands`, `TABNINE.md` |
|
||||
| `trae` | `.trae/skills`, `.trae/rules/project_rules.md` |
|
||||
| `zcode` | `.zcode/skills`, `ZCODE.md` |
|
||||
| `windsurf` | `.windsurf/workflows`, `.windsurf/rules/specify-rules.md` |
|
||||
|
||||
Integrations that share a context file or command directory with another integration, require dynamic install paths such as `--commands-dir`, or merge shared tool settings are not declared safe by default. They can still be installed alongside another integration with `--force`.
|
||||
|
||||
@@ -283,7 +212,7 @@ Run `specify integration list` to see all available integrations with their keys
|
||||
|
||||
### Do I need the AI coding agent installed to use an integration?
|
||||
|
||||
CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be installed. IDE-based integrations (like Cursor) work through the IDE itself. Some agents like GitHub Copilot support both IDE and CLI usage. `specify integration list` shows which type each integration is.
|
||||
CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be installed. IDE-based integrations (like Windsurf, Cursor) work through the IDE itself. Some agents like GitHub Copilot support both IDE and CLI usage. `specify integration list` shows which type each integration is.
|
||||
|
||||
### When should I use `upgrade` vs `switch`?
|
||||
|
||||
|
||||
@@ -137,11 +137,9 @@ catalogs:
|
||||
|
||||
## File Resolution
|
||||
|
||||
Presets can provide command files, template files (like `plan-template.md`), and script files. Each file name is evaluated independently against the priority stack, so different files can come from different layers.
|
||||
Presets can provide command files, template files (like `plan-template.md`), and script files. These are resolved at runtime using a **replace** strategy — the first match in the priority stack wins and is used entirely. Each file is looked up independently, so different files can come from different layers.
|
||||
|
||||
Templates and scripts are looked up from the stack when Spec Kit needs them. Commands use the same stack for replacement and composition, but are materialized into detected agent directories instead of being re-resolved by agents. During preset install, Spec Kit registers command files for the preset being installed; post-install and post-removal reconciliation then recomputes and writes the effective command content for affected command names based on the active stack. Agents do not re-resolve the stack each time they run a command.
|
||||
|
||||
By default, files use a **replace** strategy: the first match in the priority stack wins and is used entirely. Templates and commands can also use composition strategies: **prepend** places preset content before lower-priority content, **append** places it after lower-priority content, and **wrap** replaces `{CORE_TEMPLATE}` with lower-priority content. Scripts support **replace** and **wrap**; script wrappers use `$CORE_SCRIPT` as the placeholder.
|
||||
> **Note:** Additional composition strategies (`append`, `prepend`, `wrap`) are planned for a future release.
|
||||
|
||||
The resolution stack, from highest to lowest precedence:
|
||||
|
||||
@@ -150,6 +148,8 @@ The resolution stack, from highest to lowest precedence:
|
||||
3. **Installed extensions** — sorted by priority
|
||||
4. **Spec Kit core** — `.specify/templates/`
|
||||
|
||||
Commands are registered at install time (not resolved through the stack at runtime).
|
||||
|
||||
### Resolution Stack
|
||||
|
||||
```mermaid
|
||||
@@ -215,7 +215,7 @@ Run `specify preset resolve <name>` to trace the resolution stack and see which
|
||||
|
||||
### What's the difference between disabling and removing a preset?
|
||||
|
||||
**Disabling** (`specify preset disable`) keeps the preset installed but excludes it from future template and script resolution. Previously registered commands remain available in your AI coding agent until preset removal, so use removal when you need command changes to stop taking effect. Disabling is useful for temporarily testing template/script behavior without a preset, or comparing template/script output with and without it. Re-enable anytime with `specify preset enable`.
|
||||
**Disabling** (`specify preset disable`) keeps the preset installed but excludes its files from the resolution stack. Commands the preset registered remain available in your AI coding agent. This is useful for temporarily testing behavior without a preset, or comparing output with and without it. Re-enable anytime with `specify preset enable`.
|
||||
|
||||
**Removing** (`specify preset remove`) fully uninstalls the preset — deletes its files, unregisters its commands from your AI coding agent, and removes it from the registry.
|
||||
|
||||
|
||||
@@ -262,7 +262,6 @@ specify workflow run speckit -i spec="Build a kanban board with drag-and-drop ta
|
||||
| `command` | Invoke a Spec Kit command (e.g., `speckit.plan`) |
|
||||
| `prompt` | Send an arbitrary prompt to the AI coding agent |
|
||||
| `shell` | Execute a shell command and capture output |
|
||||
| `init` | Bootstrap a project (like `specify init`) |
|
||||
| `gate` | Pause for human approval before continuing |
|
||||
| `if` | Conditional branching (then/else) |
|
||||
| `switch` | Multi-branch dispatch on an expression |
|
||||
@@ -271,8 +270,6 @@ specify workflow run speckit -i spec="Build a kanban board with drag-and-drop ta
|
||||
| `fan-out` | Dispatch a step for each item in a list |
|
||||
| `fan-in` | Aggregate results from a fan-out step |
|
||||
|
||||
> **Security note:** a `shell` step runs a local command with **your** privileges. There is no capability sandbox — `requires` is an advisory pre-condition block (spec-kit version, integrations), not a runtime gate, so it does **not** restrict what a step can do. In particular there is no `requires.permissions` capability gate: it is rejected by validation precisely because it would imply a sandbox that does not exist. Review any catalog or downloaded workflow before running it, and use a `gate` step to require explicit approval before sensitive or destructive shell commands.
|
||||
|
||||
## Expressions
|
||||
|
||||
Steps can reference inputs and previous step outputs using `{{ expression }}` syntax:
|
||||
|
||||
@@ -35,10 +35,6 @@
|
||||
href: reference/presets.md
|
||||
- name: Workflows
|
||||
href: reference/workflows.md
|
||||
- name: Bundles
|
||||
href: reference/bundles.md
|
||||
- name: Authentication
|
||||
href: reference/authentication.md
|
||||
|
||||
# Concepts
|
||||
- name: Concepts
|
||||
@@ -57,8 +53,6 @@
|
||||
href: local-development.md
|
||||
- name: Evolving Specs
|
||||
href: guides/evolving-specs.md
|
||||
- name: Monorepos
|
||||
href: guides/monorepo.md
|
||||
|
||||
# Community
|
||||
- name: Community
|
||||
@@ -70,8 +64,6 @@
|
||||
href: community/extensions.md
|
||||
- name: Presets
|
||||
href: community/presets.md
|
||||
- name: Bundles
|
||||
href: community/bundles.md
|
||||
- name: Walkthroughs
|
||||
href: community/walkthroughs.md
|
||||
- name: Friends
|
||||
|
||||
@@ -185,7 +185,7 @@ cp -r .specify/scripts .specify/scripts-backup
|
||||
|
||||
### 3. Duplicate slash commands (IDE-based agents)
|
||||
|
||||
Some IDE-based agents (like Kilo Code, Cline) may show **duplicate slash commands** after upgrading—both old and new versions appear.
|
||||
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.
|
||||
|
||||
@@ -193,7 +193,7 @@ Some IDE-based agents (like Kilo Code, Cline) may show **duplicate slash command
|
||||
|
||||
```bash
|
||||
# Navigate to the agent's commands folder
|
||||
cd .kilocode/workflows/
|
||||
cd .kilocode/rules/
|
||||
|
||||
# List files and identify duplicates
|
||||
ls -la
|
||||
@@ -242,11 +242,11 @@ mv /tmp/constitution-backup.md .specify/memory/constitution.md
|
||||
|
||||
### Scenario 3: "I see duplicate slash commands in my IDE"
|
||||
|
||||
This happens with IDE-based agents (Kilo Code, Cline, etc.).
|
||||
This happens with IDE-based agents (Kilo Code, Windsurf, Roo Code, etc.).
|
||||
|
||||
```bash
|
||||
# Find the agent folder (example: .kilocode/workflows/)
|
||||
cd .kilocode/workflows/
|
||||
# Find the agent folder (example: .kilocode/rules/)
|
||||
cd .kilocode/rules/
|
||||
|
||||
# List all files
|
||||
ls -la
|
||||
@@ -308,7 +308,6 @@ Alternatively, run the `/speckit.specify` command which creates `.specify/featur
|
||||
ls -la .gemini/commands/ # Gemini
|
||||
ls -la .cursor/skills/ # Cursor
|
||||
ls -la .pi/prompts/ # Pi Coding Agent
|
||||
ls -la .omp/commands/ # Oh My Pi
|
||||
```
|
||||
|
||||
3. **Check agent-specific setup:**
|
||||
@@ -428,7 +427,7 @@ The `specify` CLI tool is used for:
|
||||
- **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/`, `.pi/prompts/`, `.omp/commands/`, etc.). Your AI coding agent reads these command files directly—no need to run `specify` again.
|
||||
Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/speckit.plan`, etc.) are **permanently installed** in your project's agent folder (`.claude/`, `.github/prompts/`, `.pi/prompts/`, etc.). Your AI coding agent reads these command files directly—no need to run `specify` again.
|
||||
|
||||
**If your agent isn't recognizing slash commands:**
|
||||
|
||||
@@ -443,9 +442,6 @@ Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/s
|
||||
|
||||
# For Pi
|
||||
ls -la .pi/prompts/
|
||||
|
||||
# For Oh My Pi
|
||||
ls -la .omp/commands/
|
||||
```
|
||||
|
||||
2. **Restart your IDE/editor completely** (not just reload window)
|
||||
|
||||
@@ -320,7 +320,6 @@ A: Extensions should be free and open-source. Commercial support/services are al
|
||||
"author": "string (required)",
|
||||
"version": "string (required, semver)",
|
||||
"download_url": "string (required, valid URL)",
|
||||
"sha256": "string (optional, SHA-256 hex digest of the archive at download_url; verified before install)",
|
||||
"repository": "string (required, valid URL)",
|
||||
"homepage": "string (optional, valid URL)",
|
||||
"documentation": "string (optional, valid URL)",
|
||||
|
||||
@@ -6,17 +6,15 @@ It owns the lifecycle of the managed section delimited by the configurable start
|
||||
|
||||
## Why an extension?
|
||||
|
||||
Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Keeping this behavior in a dedicated, **opt-in** extension lets users:
|
||||
Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Extracting this behavior into a dedicated extension lets users:
|
||||
|
||||
- **Choose whether to install it at all** — `specify init` does not install it. Add it explicitly when you want Spec Kit to manage the agent context file; if it is absent or disabled, Spec Kit never creates or modifies that file.
|
||||
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — the bundled scripts honor the `context_markers` value.
|
||||
- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file.
|
||||
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — both the Python layer and the bundled scripts honor the same `context_markers` value.
|
||||
- **Synchronize multiple agent anchors** by setting `context_files` when a project intentionally uses more than one coding agent context file, such as `AGENTS.md` and `CLAUDE.md`.
|
||||
- **Refresh on demand** by running the `speckit.agent-context.update` command in your agent, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`). Invoke it using your agent's slash-command separator — `/speckit.agent-context.update` for dot-separator agents or `/speckit-agent-context-update` for hyphen-separator agents (e.g. Forge, Cline).
|
||||
- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`).
|
||||
|
||||
## Commands
|
||||
|
||||
The command ID below is canonical. When invoking it as a slash command, use your agent's separator: `/speckit.agent-context.update` for dot-separator agents or `/speckit-agent-context-update` for hyphen-separator agents (e.g. Forge, Cline).
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. |
|
||||
@@ -42,7 +40,7 @@ context_markers:
|
||||
end: "<!-- SPECKIT END -->"
|
||||
```
|
||||
|
||||
- `context_file` — the project-relative path to the coding agent context file. When empty, the bundled update scripts self-seed it by looking up the active integration's key in this extension's own `agent-context-defaults.json` map. The Specify CLI is never consulted.
|
||||
- `context_file` — the project-relative path to the coding agent context file, written by `specify init` and `specify integration install`.
|
||||
- `context_files` — optional project-relative paths to multiple coding agent context files. When non-empty, the list takes precedence over `context_file`. Absolute paths, backslash separators, and `..` path segments are rejected.
|
||||
- `context_markers.start` / `.end` — the delimiters around the managed section. Edit these to use custom markers.
|
||||
|
||||
@@ -64,4 +62,5 @@ pip install pyyaml
|
||||
specify extension disable agent-context
|
||||
```
|
||||
|
||||
When disabled (or never installed), Spec Kit performs no agent context file creation, updates, or removal — the extension's bundled scripts are the only code that ever touches the managed section. The Specify CLI carries no agent-context state at all: it never reads this config, never resolves a context file, and the `__CONTEXT_FILE__` placeholder (if present in any template) is left untouched. All context-file knowledge — including the per-agent default mapping in `agent-context-defaults.json` — lives entirely within this extension, so disabling it is a complete opt-out.
|
||||
When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
|
||||
Disabled projects also ignore stale `context_files` values during command rendering so disabling the extension remains a complete opt-out.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"_comment": "Default coding agent context file per integration, owned by the agent-context extension. Used to self-seed agent-context-config.yml when it declares no context_file/context_files. Keyed by the Spec Kit integration key recorded in .specify/init-options.json. This mapping is independent of the Specify CLI by design.",
|
||||
"agents": {
|
||||
"agy": "AGENTS.md",
|
||||
"amp": "AGENTS.md",
|
||||
"auggie": ".augment/rules/specify-rules.md",
|
||||
"bob": "AGENTS.md",
|
||||
"claude": "CLAUDE.md",
|
||||
"cline": ".clinerules/specify-rules.md",
|
||||
"codebuddy": "CODEBUDDY.md",
|
||||
"codex": "AGENTS.md",
|
||||
"copilot": ".github/copilot-instructions.md",
|
||||
"cursor-agent": ".cursor/rules/specify-rules.mdc",
|
||||
"devin": "AGENTS.md",
|
||||
"firebender": ".firebender/rules/specify-rules.mdc",
|
||||
"forge": "AGENTS.md",
|
||||
"gemini": "GEMINI.md",
|
||||
"generic": "AGENTS.md",
|
||||
"goose": "AGENTS.md",
|
||||
"hermes": "AGENTS.md",
|
||||
"junie": ".junie/AGENTS.md",
|
||||
"kilocode": ".kilocode/rules/specify-rules.md",
|
||||
"kimi": "AGENTS.md",
|
||||
"kiro-cli": "AGENTS.md",
|
||||
"lingma": ".lingma/rules/specify-rules.md",
|
||||
"omp": "AGENTS.md",
|
||||
"opencode": "AGENTS.md",
|
||||
"pi": "AGENTS.md",
|
||||
"qodercli": "QODER.md",
|
||||
"qwen": "QWEN.md",
|
||||
"rovodev": "AGENTS.md",
|
||||
"shai": "SHAI.md",
|
||||
"tabnine": "TABNINE.md",
|
||||
"trae": ".trae/rules/project_rules.md",
|
||||
"vibe": "AGENTS.md",
|
||||
"windsurf": ".windsurf/rules/specify-rules.md",
|
||||
"zcode": "ZCODE.md",
|
||||
"zed": "AGENTS.md"
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@
|
||||
#
|
||||
# Usage: update-agent-context.sh [plan_path]
|
||||
#
|
||||
# When `plan_path` is omitted, the script derives it from `.specify/feature.json`
|
||||
# (written by /speckit-specify). Falls back to the most recently modified
|
||||
# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet.
|
||||
# When `plan_path` is omitted, the script picks the most recently modified
|
||||
# `specs/*/plan.md` if any exist, otherwise emits the section without a
|
||||
# concrete plan path.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -59,14 +59,7 @@ case "$(uname -s 2>/dev/null || true)" in
|
||||
esac
|
||||
|
||||
# Parse extension config once; emit context files as JSON, followed by marker strings.
|
||||
#
|
||||
# NOTE (bash 3.2 / macOS portability): the embedded Python heredocs below run
|
||||
# inside $(...) command substitution. bash 3.2 (the system /bin/bash on macOS)
|
||||
# mis-parses a single-quote/apostrophe in a heredoc body nested in $(...),
|
||||
# failing with "unexpected EOF while looking for matching `''". Keep these
|
||||
# $(...)-nested heredoc bodies free of apostrophes (use double quotes in Python
|
||||
# string literals and avoid contractions in comments).
|
||||
if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" "$PROJECT_ROOT" <<'PY'
|
||||
if ! _raw_opts="$("$_python" - "$EXT_CONFIG" "$_case_insensitive_context_files" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
try:
|
||||
@@ -102,67 +95,24 @@ def get_str(obj, *keys):
|
||||
context_files = []
|
||||
seen_context_files = set()
|
||||
case_insensitive = sys.argv[2] == "1" or sys.platform.startswith(("win32", "cygwin"))
|
||||
def add_context_file(value):
|
||||
if not isinstance(value, str):
|
||||
return
|
||||
candidate = value.strip()
|
||||
if not candidate:
|
||||
return
|
||||
key = candidate.casefold() if case_insensitive else candidate
|
||||
if key in seen_context_files:
|
||||
return
|
||||
context_files.append(candidate)
|
||||
seen_context_files.add(key)
|
||||
raw_files = data.get("context_files")
|
||||
if isinstance(raw_files, list):
|
||||
for value in raw_files:
|
||||
add_context_file(value)
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
candidate = value.strip()
|
||||
if not candidate:
|
||||
continue
|
||||
key = candidate.casefold() if case_insensitive else candidate
|
||||
if key in seen_context_files:
|
||||
continue
|
||||
context_files.append(candidate)
|
||||
seen_context_files.add(key)
|
||||
if not context_files:
|
||||
add_context_file(get_str(data, "context_file"))
|
||||
if not context_files:
|
||||
# Self-seed: the agent-context extension manages its own lifecycle, so when
|
||||
# its config declares no target, it derives one from the active integration
|
||||
# recorded in init-options.json, mapped through the bundled
|
||||
# agent-context-defaults.json file. This is independent of the Specify CLI
|
||||
# by design; nothing here imports specify_cli.
|
||||
project_root = sys.argv[3] if len(sys.argv) > 3 else "."
|
||||
integration_key = ""
|
||||
try:
|
||||
with open(
|
||||
f"{project_root}/.specify/init-options.json", "r", encoding="utf-8"
|
||||
) as fh:
|
||||
opts = json.load(fh)
|
||||
if isinstance(opts, dict):
|
||||
value = opts.get("integration") or opts.get("ai") or ""
|
||||
integration_key = value if isinstance(value, str) else ""
|
||||
except Exception:
|
||||
integration_key = ""
|
||||
if integration_key:
|
||||
defaults_path = (
|
||||
f"{project_root}/.specify/extensions/agent-context/"
|
||||
"agent-context-defaults.json"
|
||||
)
|
||||
mapping = {}
|
||||
try:
|
||||
with open(defaults_path, "r", encoding="utf-8") as fh:
|
||||
loaded = json.load(fh)
|
||||
agents = loaded.get("agents", {}) if isinstance(loaded, dict) else {}
|
||||
mapping = agents if isinstance(agents, dict) else {}
|
||||
except Exception:
|
||||
print(
|
||||
"agent-context: unable to read %s; cannot self-seed the context "
|
||||
"file. Set context_file in the extension config." % defaults_path,
|
||||
file=sys.stderr,
|
||||
)
|
||||
mapping = {}
|
||||
add_context_file(mapping.get(integration_key, "") or "")
|
||||
if not context_files:
|
||||
print(
|
||||
"agent-context: no default context file is known for integration "
|
||||
"%s. Set context_file in the extension config to choose one."
|
||||
% integration_key,
|
||||
file=sys.stderr,
|
||||
)
|
||||
raw_file = get_str(data, "context_file")
|
||||
candidate = raw_file.strip()
|
||||
if candidate:
|
||||
context_files.append(candidate)
|
||||
print(json.dumps(context_files))
|
||||
print(get_str(data, "context_markers", "start"))
|
||||
print(get_str(data, "context_markers", "end"))
|
||||
@@ -252,78 +202,23 @@ unset _cf_parts _seg
|
||||
|
||||
PLAN_PATH="${1:-}"
|
||||
if [[ -z "$PLAN_PATH" ]]; then
|
||||
# Prefer .specify/feature.json (written by /speckit-specify) over mtime heuristic.
|
||||
_feature_json="$PROJECT_ROOT/.specify/feature.json"
|
||||
if [[ -f "$_feature_json" ]]; then
|
||||
_feature_dir="$("$_python" - "$_feature_json" <<'PY'
|
||||
import sys, json
|
||||
try:
|
||||
with open(sys.argv[1], encoding="utf-8") as fh:
|
||||
d = json.load(fh)
|
||||
val = d.get("feature_directory", "")
|
||||
print(val if isinstance(val, str) else "")
|
||||
except Exception:
|
||||
print("")
|
||||
PY
|
||||
)"
|
||||
# Normalize backslashes (written by PS on Windows) to forward slashes before path ops.
|
||||
_feature_dir="$(printf '%s' "$_feature_dir" | tr '\\' '/')"
|
||||
_feature_dir="${_feature_dir%/}"
|
||||
if [[ -n "$_feature_dir" ]]; then
|
||||
# feature_directory may be relative or absolute (absolute paths outside PROJECT_ROOT
|
||||
# are preserved as-is by _persist_feature_json in common.sh).
|
||||
# Also match drive-qualified paths (C:/...) written by PowerShell on Windows.
|
||||
if [[ "$_feature_dir" == /* ]] || [[ "$_feature_dir" =~ ^[A-Za-z]:/ ]]; then
|
||||
_candidate="$_feature_dir/plan.md"
|
||||
else
|
||||
_candidate="$PROJECT_ROOT/$_feature_dir/plan.md"
|
||||
fi
|
||||
if [[ -f "$_candidate" ]]; then
|
||||
# Resolve symlinks before comparing so paths like /var/… vs /private/var/…
|
||||
# (macOS) are treated as equivalent. Mirrors the mtime-fallback approach.
|
||||
PLAN_PATH="$("$_python" - "$PROJECT_ROOT" "$_candidate" <<'PY'
|
||||
import sys
|
||||
# Pick the most recently modified plan.md one level deep (specs/<feature>/plan.md).
|
||||
# Use find + sort by modification time to avoid ls/head fragility with
|
||||
# spaces in paths or SIGPIPE from pipefail.
|
||||
_plan_abs="$("$_python" - "$PROJECT_ROOT" <<'PY'
|
||||
import sys, os
|
||||
from pathlib import Path
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
cand = Path(sys.argv[2]).resolve()
|
||||
try:
|
||||
print(cand.relative_to(root).as_posix())
|
||||
except ValueError:
|
||||
# Outside project root: emit the resolved path in POSIX form.
|
||||
# as_posix() converts backslashes correctly on native Windows Python.
|
||||
print(cand.as_posix())
|
||||
PY
|
||||
)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Fall back to mtime only when feature.json is absent or its plan does not exist yet.
|
||||
# Python emits a project-relative POSIX path directly to avoid bash prefix-strip
|
||||
# issues with backslash paths on Windows (Git bash / MSYS2).
|
||||
if [[ -z "$PLAN_PATH" ]]; then
|
||||
_plan_rel="$("$_python" - "$PROJECT_ROOT" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
root = Path(sys.argv[1]).resolve()
|
||||
specs = root / "specs"
|
||||
specs = Path(sys.argv[1]) / "specs"
|
||||
plans = sorted(
|
||||
specs.glob("*/plan.md"),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
if plans:
|
||||
try:
|
||||
print(plans[0].relative_to(root).as_posix())
|
||||
except ValueError:
|
||||
print("")
|
||||
else:
|
||||
print("")
|
||||
print(plans[0] if plans else "")
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$_plan_rel" ]]; then
|
||||
PLAN_PATH="$_plan_rel"
|
||||
fi
|
||||
if [[ -n "$_plan_abs" ]]; then
|
||||
PLAN_PATH="${_plan_abs#"$PROJECT_ROOT/"}"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -345,58 +240,11 @@ for CONTEXT_FILE in "${CONTEXT_FILES[@]}"; do
|
||||
mkdir -p "$(dirname "$CTX_PATH")"
|
||||
|
||||
"$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY'
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import sys, os
|
||||
ctx_path, start, end, section_path = sys.argv[1:5]
|
||||
with open(section_path, "r", encoding="utf-8") as fh:
|
||||
section = fh.read().rstrip("\n") + "\n"
|
||||
|
||||
|
||||
def ensure_mdc_frontmatter(content):
|
||||
"""Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``.
|
||||
|
||||
Cursor only auto-loads ``.mdc`` rule files that carry frontmatter with
|
||||
``alwaysApply: true``. Prepend it when missing, or repair the value while
|
||||
preserving any existing frontmatter comments/formatting.
|
||||
"""
|
||||
leading_ws = len(content) - len(content.lstrip())
|
||||
leading = content[:leading_ws]
|
||||
stripped = content[leading_ws:]
|
||||
|
||||
if not stripped.startswith("---"):
|
||||
return "---\nalwaysApply: true\n---\n\n" + content
|
||||
|
||||
match = re.match(
|
||||
r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)",
|
||||
stripped,
|
||||
re.DOTALL,
|
||||
)
|
||||
if not match:
|
||||
return "---\nalwaysApply: true\n---\n\n" + content
|
||||
|
||||
opening, fm_text, closing, sep, rest = match.groups()
|
||||
newline = "\r\n" if "\r\n" in opening else "\n"
|
||||
|
||||
if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text):
|
||||
return content
|
||||
|
||||
if re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text):
|
||||
fm_text = re.sub(
|
||||
r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$",
|
||||
r"\1alwaysApply: true\2",
|
||||
fm_text,
|
||||
count=1,
|
||||
)
|
||||
elif fm_text.strip():
|
||||
fm_text = fm_text + newline + "alwaysApply: true"
|
||||
else:
|
||||
fm_text = "alwaysApply: true"
|
||||
|
||||
return f"{leading}{opening}{fm_text}{closing}{sep}{rest}"
|
||||
|
||||
|
||||
if os.path.exists(ctx_path):
|
||||
with open(ctx_path, "r", encoding="utf-8-sig") as fh:
|
||||
content = fh.read()
|
||||
@@ -426,8 +274,6 @@ else:
|
||||
new_content = section
|
||||
|
||||
new_content = new_content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
if ctx_path.casefold().endswith(".mdc"):
|
||||
new_content = ensure_mdc_frontmatter(new_content)
|
||||
with open(ctx_path, "wb") as fh:
|
||||
fh.write(new_content.encode("utf-8"))
|
||||
PY
|
||||
|
||||
@@ -9,10 +9,6 @@
|
||||
# .specify/extensions/agent-context/agent-context-config.yml
|
||||
#
|
||||
# Usage: update-agent-context.ps1 [plan_path]
|
||||
#
|
||||
# When `plan_path` is omitted, the script derives it from `.specify/feature.json`
|
||||
# (written by /speckit-specify). Falls back to the most recently modified
|
||||
# `specs/*/plan.md` only when feature.json is absent or its plan does not exist yet.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -20,56 +16,6 @@ param(
|
||||
[string]$PlanPath
|
||||
)
|
||||
|
||||
function Add-MdcFrontmatter {
|
||||
<#
|
||||
Ensure .mdc content has YAML frontmatter with alwaysApply: true.
|
||||
|
||||
Cursor only auto-loads .mdc rule files that carry frontmatter with
|
||||
alwaysApply: true. Prepend it when missing, or repair the value while
|
||||
preserving any existing frontmatter comments/formatting.
|
||||
#>
|
||||
param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$Content)
|
||||
|
||||
$leading = ''
|
||||
$stripped = $Content
|
||||
$m = [regex]::Match($Content, '^\s*')
|
||||
if ($m.Success) {
|
||||
$leading = $m.Value
|
||||
$stripped = $Content.Substring($m.Length)
|
||||
}
|
||||
|
||||
if (-not $stripped.StartsWith('---')) {
|
||||
return "---`nalwaysApply: true`n---`n`n" + $Content
|
||||
}
|
||||
|
||||
$fm = [regex]::Match($stripped, '^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)', [System.Text.RegularExpressions.RegexOptions]::Singleline)
|
||||
if (-not $fm.Success) {
|
||||
return "---`nalwaysApply: true`n---`n`n" + $Content
|
||||
}
|
||||
|
||||
$opening = $fm.Groups[1].Value
|
||||
$fmText = $fm.Groups[2].Value
|
||||
$closing = $fm.Groups[3].Value
|
||||
$sep = $fm.Groups[4].Value
|
||||
$rest = $fm.Groups[5].Value
|
||||
$newline = if ($opening.Contains("`r`n")) { "`r`n" } else { "`n" }
|
||||
|
||||
if ([regex]::IsMatch($fmText, '(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$')) {
|
||||
return $Content
|
||||
}
|
||||
|
||||
if ([regex]::IsMatch($fmText, '(?m)^[ \t]*alwaysApply[ \t]*:')) {
|
||||
$alwaysApplyRegex = [regex]'(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$'
|
||||
$fmText = $alwaysApplyRegex.Replace($fmText, '${1}alwaysApply: true${2}', 1)
|
||||
} elseif ($fmText.Trim()) {
|
||||
$fmText = $fmText + $newline + 'alwaysApply: true'
|
||||
} else {
|
||||
$fmText = 'alwaysApply: true'
|
||||
}
|
||||
|
||||
return "$leading$opening$fmText$closing$sep$rest"
|
||||
}
|
||||
|
||||
function Get-ConfigValue {
|
||||
param(
|
||||
[AllowNull()][object]$Object,
|
||||
@@ -180,26 +126,14 @@ if (-not (Test-Path -LiteralPath $ExtConfig)) {
|
||||
$Options = $null
|
||||
if (Get-Command ConvertFrom-Yaml -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
$Options = Get-Content -LiteralPath $ExtConfig -Raw -Encoding UTF8 | ConvertFrom-Yaml -ErrorAction Stop
|
||||
$Options = Get-Content -LiteralPath $ExtConfig -Raw | ConvertFrom-Yaml -ErrorAction Stop
|
||||
} catch {
|
||||
# fall through to ConvertFrom-Json fallback
|
||||
# fall through to Python fallback
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $Options) {
|
||||
# ConvertFrom-Yaml unavailable or failed; try ConvertFrom-Json (no external deps,
|
||||
# works when the config file is valid JSON, which is a subset of YAML).
|
||||
try {
|
||||
$raw = Get-Content -LiteralPath $ExtConfig -Raw -Encoding UTF8
|
||||
$Options = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||
if (-not (Test-ConfigObject -Object $Options)) { $Options = $null }
|
||||
} catch {
|
||||
$Options = $null
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $Options) {
|
||||
# ConvertFrom-Yaml/Json unavailable or failed; fall back to Python+PyYAML.
|
||||
# ConvertFrom-Yaml unavailable or failed; fall back to Python+PyYAML.
|
||||
$pythonCmd = $null
|
||||
$pythonCandidates = @()
|
||||
if ($env:SPECKIT_PYTHON) {
|
||||
@@ -300,43 +234,6 @@ foreach ($ContextFile in $ContextFiles) {
|
||||
}
|
||||
}
|
||||
$ContextFiles = $dedupedContextFiles
|
||||
if ($ContextFiles.Count -eq 0) {
|
||||
# Self-seed: the agent-context extension owns its lifecycle, so when its
|
||||
# own config declares no target it derives one from the active integration
|
||||
# recorded in init-options.json, using the extension's OWN bundled mapping
|
||||
# (agent-context-defaults.json). Independent of the Specify CLI by design.
|
||||
$initOptionsPath = Join-Path $ProjectRoot '.specify/init-options.json'
|
||||
if (Test-Path -LiteralPath $initOptionsPath) {
|
||||
try {
|
||||
$initOpts = Get-Content -LiteralPath $initOptionsPath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||
$integrationKey = $null
|
||||
if ($initOpts.PSObject.Properties['integration'] -and $initOpts.integration) {
|
||||
$integrationKey = [string]$initOpts.integration
|
||||
} elseif ($initOpts.PSObject.Properties['ai'] -and $initOpts.ai) {
|
||||
$integrationKey = [string]$initOpts.ai
|
||||
}
|
||||
if ($integrationKey) {
|
||||
$defaultsPath = Join-Path $ProjectRoot '.specify/extensions/agent-context/agent-context-defaults.json'
|
||||
if (Test-Path -LiteralPath $defaultsPath) {
|
||||
$defaults = Get-Content -LiteralPath $defaultsPath -Raw | ConvertFrom-Json -ErrorAction Stop
|
||||
$derived = $null
|
||||
if ($defaults.PSObject.Properties['agents'] -and $defaults.agents.PSObject.Properties[$integrationKey]) {
|
||||
$derived = [string]$defaults.agents.PSObject.Properties[$integrationKey].Value
|
||||
}
|
||||
if ($derived -and -not [string]::IsNullOrWhiteSpace($derived)) {
|
||||
$ContextFiles += $derived.Trim()
|
||||
} else {
|
||||
Write-Warning ("agent-context: no default context file is known for integration '{0}'; set 'context_file' in the extension config to choose one." -f $integrationKey)
|
||||
}
|
||||
} else {
|
||||
Write-Warning ("agent-context: unable to read {0}; cannot self-seed the context file. Set 'context_file' in the extension config." -f $defaultsPath)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Non-fatal: fall through to the nothing-to-do guard below.
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ContextFiles.Count -eq 0) {
|
||||
Write-Warning 'agent-context: context_files/context_file not set in extension config; nothing to do.'
|
||||
exit 0
|
||||
@@ -383,69 +280,21 @@ if ($cm) {
|
||||
}
|
||||
|
||||
if (-not $PlanPath) {
|
||||
# Prefer .specify/feature.json (written by /speckit-specify) over mtime heuristic.
|
||||
$FeatureJson = Join-Path $ProjectRoot '.specify/feature.json'
|
||||
if (Test-Path -LiteralPath $FeatureJson) {
|
||||
try {
|
||||
$fj = Get-Content -LiteralPath $FeatureJson -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$featureDir = $fj.feature_directory
|
||||
if ($featureDir -isnot [string] -or -not $featureDir) {
|
||||
$featureDir = $null
|
||||
} else {
|
||||
$featureDir = $featureDir.TrimEnd('\', '/')
|
||||
}
|
||||
if ($featureDir) {
|
||||
# Join-Path on Unix does not treat absolute ChildPath as "wins"; check explicitly.
|
||||
if ([System.IO.Path]::IsPathRooted($featureDir)) {
|
||||
$candidatePlan = Join-Path $featureDir 'plan.md'
|
||||
} else {
|
||||
$candidatePlan = Join-Path (Join-Path $ProjectRoot $featureDir) 'plan.md'
|
||||
}
|
||||
if (Test-Path -LiteralPath $candidatePlan) {
|
||||
# Resolve ./ .. segments before relativizing (mirrors bash Path.resolve()).
|
||||
# GetFullPath is available in .NET Framework 4.x (PS 5.1 compatible).
|
||||
$resolvedPlan = [System.IO.Path]::GetFullPath($candidatePlan)
|
||||
$resolvedDir = [System.IO.Path]::GetDirectoryName($resolvedPlan)
|
||||
$normRoot = $ProjectRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
||||
$normDir = $resolvedDir.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
||||
$cmp = if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
||||
if ($normDir.StartsWith($normRoot, $cmp)) {
|
||||
$relDir = $normDir.Substring($normRoot.Length).TrimEnd('\', '/')
|
||||
$PlanPath = if ($relDir) { $relDir.Replace('\', '/') + '/plan.md' } else { 'plan.md' }
|
||||
} else {
|
||||
$PlanPath = $resolvedPlan.Replace('\', '/')
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Non-fatal: fall through to mtime heuristic.
|
||||
}
|
||||
}
|
||||
|
||||
# Fall back to mtime only when feature.json is absent or its plan does not exist yet.
|
||||
if (-not $PlanPath) {
|
||||
try {
|
||||
$specsDir = Join-Path $ProjectRoot 'specs'
|
||||
$candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } |
|
||||
Where-Object { $_ } |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if ($candidate) {
|
||||
# GetRelativePath is .NET 5+ only; strip prefix manually for PS 5.1 compat.
|
||||
# Use case-insensitive comparison on Windows only (matches common.ps1 pattern).
|
||||
$fullPath = $candidate.FullName.Replace('\', '/')
|
||||
$normRoot = $ProjectRoot.Replace('\', '/').TrimEnd('/') + '/'
|
||||
$cmp = if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
|
||||
if ($fullPath.StartsWith($normRoot, $cmp)) {
|
||||
$PlanPath = $fullPath.Substring($normRoot.Length)
|
||||
} else {
|
||||
$PlanPath = $fullPath
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
# Non-fatal: continue without a plan path.
|
||||
# Discover plan.md exactly one level deep (specs/<feature>/plan.md),
|
||||
# matching the bash glob specs/*/plan.md. Wrap in try/catch so access errors under
|
||||
# $ErrorActionPreference = 'Stop' don't abort the script.
|
||||
try {
|
||||
$specsDir = Join-Path $ProjectRoot 'specs'
|
||||
$candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } |
|
||||
Where-Object { $_ } |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if ($candidate) {
|
||||
$PlanPath = [System.IO.Path]::GetRelativePath($ProjectRoot, $candidate.FullName).Replace('\','/')
|
||||
}
|
||||
} catch {
|
||||
# Non-fatal: continue without a plan path.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,9 +347,6 @@ foreach ($ContextFile in $ContextFiles) {
|
||||
}
|
||||
|
||||
$newContent = $newContent.Replace("`r`n", "`n").Replace("`r", "`n")
|
||||
if ($ContextFile -match '\.mdc$') {
|
||||
$newContent = Add-MdcFrontmatter -Content $newContent
|
||||
}
|
||||
[System.IO.File]::WriteAllText($CtxPath, $newContent, (New-Object System.Text.UTF8Encoding($false)))
|
||||
|
||||
Write-Host "agent-context: updated $ContextFile"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-06-30T00:00:00Z",
|
||||
"updated_at": "2026-06-23T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||
"extensions": {
|
||||
"aide": {
|
||||
@@ -187,10 +187,10 @@
|
||||
"arch": {
|
||||
"name": "Architecture Workflow",
|
||||
"id": "arch",
|
||||
"description": "Generate or reverse project-level 4+1 architecture views with per-view and full-workflow commands",
|
||||
"description": "Generate or reverse project-level 4+1 architecture views as separate commands",
|
||||
"author": "bigsmartben",
|
||||
"version": "1.2.2",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.2.2.zip",
|
||||
"version": "1.2.1",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-arch/archive/refs/tags/v1.2.1.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-arch",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-arch",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-arch/blob/main/README.md",
|
||||
@@ -202,7 +202,7 @@
|
||||
"speckit_version": ">=0.8.10.dev0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 12,
|
||||
"commands": 10,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
@@ -215,7 +215,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-14T00:00:00Z",
|
||||
"updated_at": "2026-06-30T00:00:00Z"
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"architect-preview": {
|
||||
"name": "Architect Impact Previewer",
|
||||
@@ -772,40 +772,40 @@
|
||||
"companion": {
|
||||
"name": "SpecKit Companion",
|
||||
"id": "companion",
|
||||
"description": "Live spec-driven progress for SpecKit Companion — lifecycle capture, status, resume, and composable commands you can customize with hooks and recipes.",
|
||||
"description": "Live spec-driven progress for SpecKit Companion — lifecycle capture, status, resume, and a turbo pipeline profile.",
|
||||
"author": "alfredoperez",
|
||||
"version": "0.11.0",
|
||||
"download_url": "https://github.com/alfredoperez/speckit-companion/releases/download/speckit-ext-v0.11.0/companion-0.11.0.zip",
|
||||
"version": "0.3.0",
|
||||
"download_url": "https://github.com/alfredoperez/speckit-companion/releases/download/speckit-ext-v0.3.0/companion-0.3.0.zip",
|
||||
"repository": "https://github.com/alfredoperez/speckit-companion",
|
||||
"homepage": "https://github.com/alfredoperez/speckit-companion/tree/main/speckit-extension",
|
||||
"documentation": "https://github.com/alfredoperez/speckit-companion/blob/main/speckit-extension/README.md",
|
||||
"documentation": "https://github.com/alfredoperez/speckit-companion/blob/main/speckit-extension/docs/",
|
||||
"changelog": "https://github.com/alfredoperez/speckit-companion/blob/main/speckit-extension/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "visibility",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.9.5",
|
||||
"speckit_version": ">=0.8.5",
|
||||
"tools": [
|
||||
{ "name": "python3", "required": false }
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 13,
|
||||
"commands": 10,
|
||||
"hooks": 4
|
||||
},
|
||||
"tags": [
|
||||
"vscode",
|
||||
"tracking",
|
||||
"companion",
|
||||
"progress",
|
||||
"status",
|
||||
"resume",
|
||||
"configurable",
|
||||
"extensible"
|
||||
"vscode",
|
||||
"lifecycle",
|
||||
"resume"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-11T00:00:00Z",
|
||||
"updated_at": "2026-06-24T00:00:00Z"
|
||||
"updated_at": "2026-06-11T00:00:00Z"
|
||||
},
|
||||
"conduct": {
|
||||
"name": "Conduct Extension",
|
||||
@@ -1327,39 +1327,6 @@
|
||||
"created_at": "2026-04-12T15:30:00Z",
|
||||
"updated_at": "2026-04-13T14:39:00Z"
|
||||
},
|
||||
"golden-demo": {
|
||||
"name": "Golden Demo",
|
||||
"id": "golden-demo",
|
||||
"description": "Extracts acceptance criteria from specs, builds test vectors, and produces a behavioral drift report — complementary to Architecture Guard and CDD.",
|
||||
"author": "jasstt",
|
||||
"version": "0.1.1",
|
||||
"download_url": "https://github.com/jasstt/spec-kit-golden-demo/archive/refs/tags/v0.1.1.zip",
|
||||
"repository": "https://github.com/jasstt/spec-kit-golden-demo",
|
||||
"homepage": "https://github.com/jasstt/spec-kit-golden-demo",
|
||||
"documentation": "https://github.com/jasstt/spec-kit-golden-demo",
|
||||
"license": "MIT",
|
||||
"category": "docs",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 2,
|
||||
"hooks": 2
|
||||
},
|
||||
"tags": [
|
||||
"testing",
|
||||
"drift-detection",
|
||||
"behavioral-oracle",
|
||||
"tdd",
|
||||
"quality"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-24T00:00:00Z",
|
||||
"updated_at": "2026-06-24T00:00:00Z"
|
||||
},
|
||||
"harness": {
|
||||
"name": "Research Harness",
|
||||
"id": "harness",
|
||||
@@ -1440,10 +1407,10 @@
|
||||
"intake": {
|
||||
"name": "Intake",
|
||||
"id": "intake",
|
||||
"description": "Normalize PRD, design, HTML SSOT, and test-case evidence into SDD-ready intake artifacts.",
|
||||
"description": "Normalize PRD, design, and test-case evidence into SDD-ready intake artifacts.",
|
||||
"author": "bigsmartben",
|
||||
"version": "0.1.3",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.1.3.zip",
|
||||
"version": "0.1.2",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-intake/archive/refs/tags/v0.1.2.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-intake",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-intake",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-intake/blob/main/README.md",
|
||||
@@ -1461,7 +1428,7 @@
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"commands": 3,
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
@@ -1475,7 +1442,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-23T00:00:00Z",
|
||||
"updated_at": "2026-06-30T00:00:00Z"
|
||||
"updated_at": "2026-06-23T00:00:00Z"
|
||||
},
|
||||
"issue": {
|
||||
"name": "GitHub Issues Integration 2",
|
||||
@@ -1581,34 +1548,25 @@
|
||||
"id": "jira-sync",
|
||||
"description": "An idempotent, drift-aware, fail-closed reconcile engine that mirrors spec-kit specs into Jira (Epic per repo, Story per spec, Subtask per phase).",
|
||||
"author": "Ash Brener",
|
||||
"version": "0.4.0",
|
||||
"download_url": "https://github.com/ashbrener/spec-kit-jira-sync/archive/refs/tags/v0.4.0.zip",
|
||||
"version": "0.2.0",
|
||||
"download_url": "https://github.com/ashbrener/spec-kit-jira-sync/archive/refs/tags/v0.2.0.zip",
|
||||
"repository": "https://github.com/ashbrener/spec-kit-jira-sync",
|
||||
"homepage": "https://github.com/ashbrener/spec-kit-jira-sync",
|
||||
"documentation": "https://github.com/ashbrener/spec-kit-jira-sync/blob/main/README.md",
|
||||
"changelog": "https://github.com/ashbrener/spec-kit-jira-sync/blob/main/CHANGELOG.md",
|
||||
"changelog": "https://github.com/ashbrener/spec-kit-jira-sync/releases",
|
||||
"license": "MIT",
|
||||
"category": "integration",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0",
|
||||
"tools": [
|
||||
{ "name": "bash", "version": ">=4.4", "required": true },
|
||||
{ "name": "git", "required": true },
|
||||
{ "name": "curl", "required": true },
|
||||
{ "name": "jq", "required": true },
|
||||
{ "name": "gitleaks", "required": false },
|
||||
{ "name": "trufflehog", "required": false }
|
||||
]
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"commands": 2,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"issue-tracking",
|
||||
"jira",
|
||||
"tasks-sync",
|
||||
"lifecycle-mirror",
|
||||
"reconcile",
|
||||
"drift-aware"
|
||||
],
|
||||
@@ -1616,7 +1574,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-08T00:00:00Z",
|
||||
"updated_at": "2026-06-24T00:00:00Z"
|
||||
"updated_at": "2026-06-08T00:00:00Z"
|
||||
},
|
||||
"learn": {
|
||||
"name": "Learning Extension",
|
||||
@@ -2501,8 +2459,8 @@
|
||||
"id": "product",
|
||||
"description": "Generates PRFAQ, Lean PRD, stakeholder summaries, and technical designs from engineering specs.",
|
||||
"author": "d0whc3r",
|
||||
"version": "1.0.1",
|
||||
"download_url": "https://github.com/d0whc3r/spec-kit-product/releases/download/v1.0.1/product-1.0.1.zip",
|
||||
"version": "0.8.3",
|
||||
"download_url": "https://github.com/d0whc3r/spec-kit-product/releases/download/v0.8.3/product-0.8.3.zip",
|
||||
"repository": "https://github.com/d0whc3r/spec-kit-product",
|
||||
"homepage": "https://d0whc3r.github.io/spec-kit-product/",
|
||||
"documentation": "https://github.com/d0whc3r/spec-kit-product/wiki",
|
||||
@@ -2514,7 +2472,7 @@
|
||||
"speckit_version": ">=0.2.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"commands": 4,
|
||||
"hooks": 3
|
||||
},
|
||||
"tags": [
|
||||
@@ -2538,7 +2496,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-26T00:00:00Z",
|
||||
"updated_at": "2026-06-29T00:00:00Z"
|
||||
"updated_at": "2026-06-01T00:00:00Z"
|
||||
},
|
||||
"product-forge": {
|
||||
"name": "Product Forge",
|
||||
@@ -2828,46 +2786,6 @@
|
||||
"created_at": "2026-03-23T13:30:00Z",
|
||||
"updated_at": "2026-03-23T13:30:00Z"
|
||||
},
|
||||
"repository-governance": {
|
||||
"name": "Repository Governance",
|
||||
"id": "repository-governance",
|
||||
"description": "Generate project-governance projections from Spec Kit metadata",
|
||||
"author": "bigben",
|
||||
"version": "3.0.1",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-agent-governance/releases/download/v3.0.1/repository-governance-v3.0.1.zip",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-agent-governance",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-agent-governance",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/README.md",
|
||||
"changelog": "https://github.com/bigsmartben/spec-kit-agent-governance/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0",
|
||||
"tools": [
|
||||
{
|
||||
"name": "uv",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"hooks": 3
|
||||
},
|
||||
"tags": [
|
||||
"governance",
|
||||
"repository",
|
||||
"agents",
|
||||
"memory",
|
||||
"context"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-30T00:00:00Z",
|
||||
"updated_at": "2026-06-30T00:00:00Z"
|
||||
},
|
||||
"reqnroll-bdd": {
|
||||
"name": "Reqnroll BDD",
|
||||
"id": "reqnroll-bdd",
|
||||
@@ -3044,40 +2962,6 @@
|
||||
"created_at": "2026-04-20T00:00:00Z",
|
||||
"updated_at": "2026-04-20T00:00:00Z"
|
||||
},
|
||||
"roadmap": {
|
||||
"name": "Spec Roadmap",
|
||||
"id": "roadmap",
|
||||
"description": "Capture a durable spec roadmap after the constitution, then review specs against it before and after implementation so spec-specific decisions, outcomes, and constraints are never lost.",
|
||||
"author": "srobroek",
|
||||
"version": "0.1.0",
|
||||
"download_url": "https://github.com/srobroek/speckit-roadmap/archive/refs/tags/v0.1.0.zip",
|
||||
"repository": "https://github.com/srobroek/speckit-roadmap",
|
||||
"homepage": "https://github.com/srobroek/speckit-roadmap",
|
||||
"documentation": "https://github.com/srobroek/speckit-roadmap/blob/main/README.md",
|
||||
"changelog": "https://github.com/srobroek/speckit-roadmap/blob/main/CHANGELOG.md",
|
||||
"license": "Apache-2.0",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.11.6"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"hooks": 3
|
||||
},
|
||||
"tags": [
|
||||
"roadmap",
|
||||
"planning",
|
||||
"governance",
|
||||
"review",
|
||||
"spec-alignment"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-24T00:00:00Z",
|
||||
"updated_at": "2026-06-24T00:00:00Z"
|
||||
},
|
||||
"schedule": {
|
||||
"name": "Spec Kit Schedule — CP-SAT Agent Orchestrator",
|
||||
"id": "schedule",
|
||||
|
||||
@@ -280,7 +280,7 @@ generate_branch_name() {
|
||||
|
||||
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)$"
|
||||
|
||||
local clean_name=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
|
||||
local meaningful_words=()
|
||||
for word in $clean_name; do
|
||||
@@ -288,9 +288,7 @@ generate_branch_name() {
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
# Uppercase via tr (portable) rather than bash's 4+ "^^" case
|
||||
# expansion, which breaks on macOS's default bash 3.2 (bad substitution).
|
||||
elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then
|
||||
elif echo "$description" | grep -qw -- "${word^^}"; then
|
||||
meaningful_words+=("$word")
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -51,4 +51,4 @@ _git_out=$(git init -q 2>&1) || { echo "[specify] Error: git init failed: $_git_
|
||||
_git_out=$(git add . 2>&1) || { echo "[specify] Error: git add failed: $_git_out" >&2; exit 1; }
|
||||
_git_out=$(git commit --allow-empty -q -m "$COMMIT_MSG" 2>&1) || { echo "[specify] Error: git commit failed: $_git_out" >&2; exit 1; }
|
||||
|
||||
echo "[OK] Git repository initialized" >&2
|
||||
echo "✓ Git repository initialized" >&2
|
||||
|
||||
@@ -252,11 +252,7 @@ function Get-BranchName {
|
||||
if ($stopWords -contains $word) { continue }
|
||||
if ($word.Length -ge 3) {
|
||||
$meaningfulWords += $word
|
||||
} elseif ($Description -cmatch "\b$($word.ToUpper())\b") {
|
||||
# Case-sensitive (-cmatch) to mirror the bash twin's case-sensitive
|
||||
# whole-word acronym match: keep a short word only when its UPPERCASE
|
||||
# form appears in the original (an acronym). -match is case-insensitive
|
||||
# and would keep every short word.
|
||||
} elseif ($Description -match "\b$($word.ToUpper())\b") {
|
||||
$meaningfulWords += $word
|
||||
}
|
||||
}
|
||||
@@ -401,10 +397,8 @@ if ($Json) {
|
||||
$obj = [PSCustomObject]@{
|
||||
BRANCH_NAME = $branchName
|
||||
FEATURE_NUM = $featureNum
|
||||
HAS_GIT = $hasGit
|
||||
}
|
||||
# $hasGit is computed for branch-creation logic only; it is intentionally not
|
||||
# emitted so this output contract matches the bash twin: BRANCH_NAME and
|
||||
# FEATURE_NUM, plus DRY_RUN (added just below) on dry runs.
|
||||
if ($DryRun) {
|
||||
$obj | Add-Member -NotePropertyName 'DRY_RUN' -NotePropertyValue $true
|
||||
}
|
||||
@@ -412,6 +406,7 @@ if ($Json) {
|
||||
} else {
|
||||
Write-Output "BRANCH_NAME: $branchName"
|
||||
Write-Output "FEATURE_NUM: $featureNum"
|
||||
Write-Output "HAS_GIT: $hasGit"
|
||||
if (-not $DryRun) {
|
||||
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-06-23T00:00:00Z",
|
||||
"updated_at": "2026-06-22T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
|
||||
"integrations": {
|
||||
"claude": {
|
||||
@@ -48,6 +48,15 @@
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide"]
|
||||
},
|
||||
"windsurf": {
|
||||
"id": "windsurf",
|
||||
"name": "Windsurf",
|
||||
"version": "1.0.0",
|
||||
"description": "Windsurf IDE workflow integration",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide"]
|
||||
},
|
||||
"amp": {
|
||||
"id": "amp",
|
||||
"name": "Amp",
|
||||
@@ -165,6 +174,15 @@
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide"]
|
||||
},
|
||||
"roo": {
|
||||
"id": "roo",
|
||||
"name": "Roo Code",
|
||||
"version": "1.0.0",
|
||||
"description": "Roo Code IDE integration",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide"]
|
||||
},
|
||||
"rovodev": {
|
||||
"id": "rovodev",
|
||||
"name": "RovoDev ACLI",
|
||||
@@ -237,11 +255,11 @@
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli"]
|
||||
},
|
||||
"omp": {
|
||||
"id": "omp",
|
||||
"name": "Oh My Pi",
|
||||
"iflow": {
|
||||
"id": "iflow",
|
||||
"name": "iFlow CLI",
|
||||
"version": "1.0.0",
|
||||
"description": "Oh My Pi (omp) terminal coding agent prompt-based integration",
|
||||
"description": "iFlow CLI integration by iflow-ai",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli"]
|
||||
@@ -299,15 +317,6 @@
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli", "skills", "z-ai"]
|
||||
},
|
||||
"zed": {
|
||||
"id": "zed",
|
||||
"name": "Zed",
|
||||
"version": "1.0.0",
|
||||
"description": "Zed editor skills-based integration",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide", "skills"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
# Spec Kit - June 2026 Newsletter
|
||||
|
||||
This edition covers Spec Kit activity in June 2026 — a month of maturation and mainstream validation. Twenty-five releases shipped (v0.9.0 through v0.12.2), spanning four minor bumps and delivering two headline capabilities: the **`/speckit.converge` command**, which closes the loop between a spec and the code that implements it, and the new **`specify bundle` subsystem**, a role-based distribution layer that composes extensions, presets, workflows, and steps into a single installable unit. The workflow engine became programmable, the git extension went opt-in as the first real breaking change, and the ecosystem crossed **120+ community extensions**. Externally, June was the highest-volume press month on record — Microsoft's own Developer Blog published a first-party spec-driven development post, an enterprise reported 2–4× velocity gains, and 75 substantive articles appeared across 25+ languages. A summary is in the table below, followed by details.
|
||||
|
||||
| **Spec Kit Core (Jun 2026)** | **Community & Content** | **SDD Ecosystem & Next** |
|
||||
| --- | --- | --- |
|
||||
| Twenty-five releases shipped (v0.9.0–v0.12.2) with key features: the `/speckit.converge` convergence loop, the `specify bundle` role-based packaging subsystem, a programmable workflow engine (step catalog, JSON output, `from_json`), the git extension becoming opt-in (`--no-git` removed), and six new agents (Cline, rovodev, Zed, Firebender, ZCode, omp). The repo grew from ~107k to **~116,500 stars**. [\[github.com\]](https://github.com/github/spec-kit/releases) | The community extension catalog grew from 105 to **124 entries**; presets reached **23**. Microsoft's Developer Blog published a first-party SDD post naming Spec Kit as the operationalizing toolkit. June was the highest-volume press month yet — **75 substantive articles** across 25+ languages. **245 contributors** now listed. | An enterprise (SNCF Connect & Tech) reported **2–4× velocity** from SDD. Analysts and comparisons increasingly name Spec Kit "the category anchor" and agent-neutral default. Competitors differentiate on brownfield and drift; balanced reviews continue to flag review-overload and ceremony for small tasks. |
|
||||
|
||||
***
|
||||
|
||||
> **Spec-Driven Development, Institutionalized.** If May was defined by milestone 100s, June was defined by validation from outside the project. Microsoft's own Developer Blog published a first-party post presenting spec-driven development and positioning Spec Kit as the toolkit that operationalizes it. An enterprise — SNCF Connect & Tech — went on the record with **2–4× velocity gains** from adopting SDD. A record **75 substantive articles** appeared in more than 25 languages, and the recurring verdict across independent comparisons was that Spec Kit is "the category anchor" and the agent-neutral default. Meanwhile the core matured from v0.9 to v0.12: the workflow engine became genuinely programmable, the first real breaking change shipped, and the new convergence loop and bundle subsystem gave the project answers to its two most-cited gaps — drift and distribution. None of this happens without the community — the contributors, extension and preset authors, bundle builders, and practitioners writing in a dozen languages. Thank you.
|
||||
|
||||
## Spec Kit Project Updates
|
||||
|
||||
### Releases Overview
|
||||
|
||||
**v0.9.0–v0.9.5** (June 1–5) opened the month with a minor bump and five patches. The headline was **native Cline integration** (#2508) and **rovodev** support (#2539), plus the long-running effort to extract agent-context updates into a bundled, opt-in **`agent-context` extension** (#2546, closing #2398). The CLI gained **`specify self upgrade`** (#2475) and a **`--force` flag for `extension add`** (#2530). The workflow engine picked up four capabilities: running YAML files **without a project** (#2825), accepting **updated inputs on resume** (#2815), **structured JSON output** across `run`/`resume`/`status` (#2814), and a **`continue_on_error` step field** for non-halting failures (#2663). Windows compatibility hardened with UTF-8 stdout/stderr (#2817), and cursor-agent headless dispatch now works end-to-end (#2631). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
**v0.10.0–v0.10.4** (June 9–16) delivered the month's first real **breaking change**: the **git extension is now opt-in** and the long-deprecated `--no-git` flag was removed at v0.10.0 (#2873, closing #2168). A long-standing community ask landed as **per-event hook lists with priority ordering** (#2798, closing #2378), letting extensions cleanly compose multiple hooks on one event. Operators gained a **`specify integration status`** reporting command (#2674), and the extension schema picked up first-class **`category` and `effect` fields** (#2899) to natively express the `Candidate`/`Adjacent`/`Niche`/`Bridge` signals. Security-relevant fixes hardened **preset URL installs against unsafe redirects** (#2911) and preserved the Claude `SKILL.md` `argument-hint` for extension commands (#2916). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
**v0.11.0–v0.11.10** (June 16–29) was the largest release cluster of the month and centered on **workflows** and the new **convergence loop**. The **`/speckit.converge` command** shipped (#3001), and the **workflow step catalog** made workflow steps community-installable the way extensions and presets already are (#2394, closing #2216). A complementary **`init` workflow step** (#2838) lets a workflow bootstrap a project the way `specify init` does. Workflow execution became programmable: opt-in `output_format: json` exposes parsed shell stdout as `output.data` (#2963), and a new **`from_json` expression filter** (#2961) turns step outputs into typed values. The new **`bug-assess` agentic workflow** (#3023) automates bug triage from labeled issues, **Zed** joined the supported agents (#2780), and contributors gained an **integration scaffolder** (#2685). The **`specify bundle` command** made its debut here (#3070). Two Windows/PowerShell pain points closed — `specify init` no longer hangs on PowerShell 5.1 (#2938) and the 233-day-old worktree branch-numbering bug was fixed (#3054, closing #1066). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
**v0.12.0–v0.12.2** (June 29–30) closed the month with a minor bump making the **`agent-context` extension a full opt-in** (#3097) and a run of workflow-engine hardening: `max_concurrency` is now honored in fan-out via a bounded thread pool (#3224), gate validation no longer crashes on non-string options (#3233), pipe-filter detection became quote-aware (#3232), and a fan-in `wait_for` that names an unknown step is now rejected at validation (#3225). Three agents were also rationalized — **Firebender** (Android Studio / IntelliJ, #3077, closing #1548), **ZCode** (Z.AI, #3063), and **omp** (#3107) joined earlier in the run, while **Windsurf** was absorbed into Cognition Devin (#3168) and **iflow** was retired as discontinued (#3166). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### The Convergence Loop: `/speckit.converge`
|
||||
|
||||
The most significant addition to the SDD workflow since the core commands themselves, **`/speckit.converge`** (#3001) adds a ninth step that runs *after* `/speckit.implement` and answers the single most-cited concern in every review of the project: *does the code actually match the spec?*
|
||||
|
||||
Converge reads `spec.md`, `plan.md`, and `tasks.md` as the **sole source of intent** — with the constitution as governing constraints — assesses the current state of the code, and appends any remaining unbuilt work as new, traceable tasks. It is deliberately **not** a diff or git tool: it evaluates the *present* state of the code relative to the feature's artifacts, with no branch comparison and no history. Findings are classified by **gap type** — `missing` (absent entirely), `partial` (present but incomplete), `contradicts` (conflicts with intent or a constitution MUST), or `unrequested` (work the spec never called for) — and graded by severity, with a constitution-MUST violation always the highest.
|
||||
|
||||
Its defining design choice is that it is **append-only and never rewrites**. Its only write is a new `## Phase N: Convergence` section at the bottom of `tasks.md`; it never modifies the spec or plan, never renumbers existing tasks, and never touches application code — completing the appended tasks remains the job of `/speckit.implement`. When the codebase already satisfies everything, it leaves `tasks.md` byte-for-byte unchanged and simply reports **"✅ Converged."** Each appended task carries a `source-ref` (e.g. `FR-003`, `SC-002`, `US1/AC2`, a plan decision, or a constitution article), preserving traceability from requirement to remediation.
|
||||
|
||||
The result is an **iterative convergence loop** — converge → implement → converge — that runs until no gaps remain. It also smooths migration from OpenSpec by giving Spec Kit a first-class verify-and-close-the-gap step (#2673), directly answering the drift-and-verification demand the community had been expressing through extensions like Architecture Guard, Spec Trace, and the various drift-control tools. The command is now documented in the quickstart and the evolving-specs guide. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/quickstart.md)
|
||||
|
||||
### The Bundle Subsystem: `specify bundle`
|
||||
|
||||
June's second headline was the debut of **bundles** (#3070), a distribution and composition layer that sits above the existing primitives. Where extensions, presets, workflows, and steps are the building blocks, a **bundle is a curated, versioned, role-based stack** that declares everything a team or role needs and installs it in a single step. Crucially, a bundle adds *no new runtime behavior of its own* — it composes what already exists through each component's own machinery, so there is nothing new to learn at execution time.
|
||||
|
||||
A bundle is described by a **`bundle.yml` manifest**: metadata (`id`, `name`, `version`, `role`, `author`, `license`), a `requires` block (minimum `speckit_version`, tools, MCP servers), and a `provides` block listing the exact extensions, presets (with `priority` and composition `strategy`), steps, and workflows it installs — each pinned to a version. The first example bundles ship four roles: **developer, product-manager, business-analyst, and security-researcher**.
|
||||
|
||||
The subcommand surface is a full package-manager experience: `search` and `info` (which previews the **fully expanded component set** with pinned versions and a `verified`-vs-`community` trust indicator before you install), `install`, `update` (`--all`), `remove`, `list`, `init`, `validate`, `build` (produces a single versioned `.zip` artifact), `publish`, and `catalog` management (`list`/`add`/`remove` sources). Installs are **idempotent with full provenance tracking**, so a bundle can be cleanly removed or refreshed later; `remove` uninstalls only the components a bundle contributed, leaving anything another installed bundle still needs in place. If run in a directory that isn't yet a Spec Kit project, `install` and `init` **bootstrap one first**, so a fresh checkout reaches a working state in a single command. The only cross-bundle conflict point checked at install time is the active integration.
|
||||
|
||||
Bundles are discovered through the same priority-ordered catalog stack (project, user, and built-in scopes) as every other component, and by the end of the month they had become a **fourth community-submittable artifact type** alongside extensions, presets, and workflows, via a dedicated submission path (#3162). Bundles are the project's answer to the "how do I distribute a whole role setup?" question — the composability story that ties the entire catalog together. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/reference/bundles.md)
|
||||
|
||||
### The Workflow Engine Matures
|
||||
|
||||
Beyond converge and bundles, June was the month the **workflow engine grew up**. The **step catalog** (#2394) made steps community-distributable; the **`init` step** (#2838) let workflows bootstrap projects; **JSON output** (#2963) and the **`from_json` filter** (#2961) made step outputs consumable as typed data; and the **`bug-assess`** agentic workflow (#3023) became the first shipped end-to-end automation built on the engine. Late-month hardening added bounded-concurrency fan-out (#3224), quote-aware expression parsing (#3232, #3197), stricter gate and `wait_for` validation (#3233, #3225), and correct non-zero exit codes on failed or aborted runs (#2959). The engine that began as a fixed seven-step sequence is now a programmable, community-extensible automation substrate. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### Architecture & Refactoring
|
||||
|
||||
The **`__init__.py` decomposition series** advanced from 4/8 to **7/8** during June. PR 5/8 co-located integration commands in the `integrations/` domain directory (#2720), PR 6/8 extracted preset command handlers into `presets/_commands.py` (#2826), and PR 7/8 moved extension command handlers into `extensions/_commands.py` (#3014). The systematic extraction continues to improve contributor onboarding and test isolation, with one part remaining. Dead HTTP helpers (`open_github_url`, `_StripAuthOnRedirect`) were removed following the preset URL-install hardening (#2883). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### Bug Fixes and Security
|
||||
|
||||
Twenty-five releases produced a heavy cadence of fixes, concentrated on **cross-platform parity** and **workflow robustness**. Windows/PowerShell saw the most attention: the PowerShell 5.1 init hang (#2938), UTF-8 stdout/stderr (#2817), stderr routing for `check-prerequisites.ps1` (#3123), case-sensitive branch-name acronym parity (#3129), and several bash-parity script fixes (#3196, #3198, #3230, #3231). Workflow correctness improved with loud failures on unknown expression filters (#3074), rejection of phantom permissions gates (#3079), and preserved commas inside quoted list literals (#3134). Long-standing bugs closed include the 233-day worktree branch-numbering repeat (#1066) and the extension-command registration gap on integration upgrade (#2886).
|
||||
|
||||
Security and supply-chain work was a distinct theme this month. **Preset URL installs were hardened against unsafe redirects** (#2911), **`run_command` now rejects `shell=True`** (#3132), **command-registration path handling was hardened** (#3088), **CI actions were pinned to commit SHAs with shellcheck added** (#3126), **catalog archives are verified by sha256 before install** (#3080), the **extension self-install path can no longer delete its source directory** (#2991), **per-extension failures are isolated** so one bad extension can't drop the rest (#2951), and **host-less catalog URLs are now rejected** in the base and preset validators (#3209). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### The Extension & Preset Ecosystem
|
||||
|
||||
The community extension catalog grew from 105 to **124 entries** during June — nineteen net additions across four steady weeks. Community presets grew from 21 to **23**.
|
||||
|
||||
Notable new extensions by category:
|
||||
|
||||
- **Verification & drift**: Golden Demo executable-reference + behavioral-drift detection, Coding Standards Drift Control, Spec Trace spec-to-code traceability
|
||||
- **External trackers & round-trip**: Linear integration (`spec-kit-linear`), Jira Integration via sync engine, Tasks to GitHub Project
|
||||
- **Autonomy & loops**: Loop Engineering (safe maker/checker agent loops), Research Harness
|
||||
- **Token & context economy**: Token Economy (routing, measured savings, context audits)
|
||||
- **Visibility & artifacts**: Spec Kit TLDR review dashboard, Data Model Diagram (Mermaid ER diagrams), Spec Roadmap
|
||||
- **Intake & discovery**: Improve (audit a codebase into prioritized spec prompts), Intake (structured requirement intake), Spec Kit Discovery
|
||||
- **Multi-project**: Multi-Sites Spec Kit, RAG Azure Builder, SpecKit Companion
|
||||
|
||||
The catalog also showed strong maintenance activity: **Linear Integration** advanced through several releases (to v0.7.0), **DocGuard — CDD Enforcement** progressed to v0.28.0, the **Superpowers** bridges continued rapid iteration, and **Architecture Guard**, **Security Review**, **Product Forge**, **MemoryLint**, and **Multi-Model Review** all shipped updates. New presets included **Command Density** and **SicarioSpec Core**, and the governance-preset family (a11y, agent-parity, cross-platform, iSAQB-architecture, architecture, security) received a coordinated round of updates. [\[github.com\]](https://github.github.io/spec-kit/community/extensions.html)
|
||||
|
||||
### Documentation & Docs Site
|
||||
|
||||
June closed several long-standing documentation gaps. A **guide for handling complex features** landed (#3004), and **evolving specs in existing projects** was formally documented (#2902, closing the 243-day #916). **Spec-persistence models** were documented (#2856), a **monorepo guide** was added (#3084), and **GitHub Copilot CLI guidance** joined the README (#2891). Reference docs for the new **bundles** and **integration catalog** subcommands were added (#3206, #3174), agent disclosure was strengthened to cover commits and per-round comments (#3071), and preset submissions now require a usage README with Spec Kit CLI syntax (#3104). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
## Community & Content
|
||||
|
||||
### Microsoft's First-Party Endorsement
|
||||
|
||||
On **June 10**, the **Microsoft Developer Blog** published *"Spec-Driven Development: A Spec-First Approach to AI-Native Engineering"* by Apoorv Gupta (Principal Software Engineer, Microsoft) — the first first-party, non-maintainer post to present SDD and position **GitHub Spec Kit as the toolkit that operationalizes it**. The article covers the seven-step lifecycle and walks through three real greenfield and brownfield case studies, distilling the practice to a single line: **"spec quality = output quality."** Coming from Microsoft's own developer platform rather than the maintainers, it was the month's clearest signal that spec-driven development has moved from community experiment to institutionally endorsed practice. [\[developer.microsoft.com\]](https://developer.microsoft.com/blog/spec-driven-development-ai-native-engineering)
|
||||
|
||||
### Press and Industry Coverage
|
||||
|
||||
June was the **highest-volume coverage month on record — 75 substantive articles** across more than 25 languages.
|
||||
|
||||
**Xebia / XPRT Magazine #21** (Hidde de Smet & Emanuele Bartolesi, June 17) published a 32-minute full six-command walkthrough covering both greenfield and brownfield, honest about markdown-review overhead and where spec quality becomes the bottleneck. [\[xebia.com\]](https://xebia.com/blog/building-software-with-spec-kit/)
|
||||
|
||||
**Design News** (Jacob Beningo, June 26) published *"A Practical Guide to Spec-Driven Development with AI"*, explaining SDD for embedded engineers and highlighting Spec Kit as the agent-agnostic reference tool — notable for reaching an audience well outside the usual web-developer sphere. [\[designnews.com\]](https://www.designnews.com/embedded-systems/a-practical-guide-to-spec-driven-development-with-ai)
|
||||
|
||||
**SSOJet** (David Brown, June 26) surveyed seven SDD tools and named GitHub Spec Kit **"the category anchor and default agent-neutral pick."** [\[ssojet.com\]](https://ssojet.com/blog/best-spec-driven-development-tools)
|
||||
|
||||
**The Tokenizer** (Sairam Sundaresan, June 12), a curated AI newsletter, spotlighted `github/spec-kit` as the structured alternative to one-shot prompting alongside coverage of Spotify and DeepMind. [\[artofsaience.com\]](https://newsletter.artofsaience.com/p/spotifys-agent-context-layer-deepminds)
|
||||
|
||||
**FintechExtra** (June 1) published a factual v0.9.x release-notes summary covering the agent-context migration to an opt-in extension, UTF-8 CLI encoding fixes, JSON workflow output, and headless CLI dispatch. [\[fintechextra.com\]](https://www.fintechextra.com/news/spec-kit-v090-agent-context-migration-to-extension-608)
|
||||
|
||||
### Enterprise Adoption
|
||||
|
||||
**SNCF Connect & Tech** — the technology arm of France's national railway — went on the record in a **CIO Online** interview (Reynald Fléchaux, June 30). CTO Emmanuel Cordente reported **2–4× velocity gains** from adopting spec-driven development via open-source frameworks it named explicitly, including Spec Kit, while candidly flagging token-cost and governance concerns. It is one of the first named-enterprise, on-the-record velocity claims for SDD. [\[cio-online.com\]](https://www.cio-online.com/actualites/lire-emmanuel-cordente-sncf-connect-et-tech--avec-le-spec-driven-development-une-vitesse-multipliee-par-2-a-4-17120.html)
|
||||
|
||||
### Developer Articles and Blog Posts
|
||||
|
||||
June's 75 articles skewed heavily multilingual, with deep hands-on series in Chinese, Japanese, and Korean, and a strong current of "which tool should I choose?" comparisons.
|
||||
|
||||
Notable English-language articles:
|
||||
|
||||
- **Achraf Ben Alaya** (Azure MVP, June 28) published an honest .NET 10 / Blazor field report praising plan→tasks decomposition and the converge loop while flagging migration pitfalls and "overwhelming" markdown output. [\[achrafbenalaya.com\]](https://achrafbenalaya.com/2026/06/28/i-tried-github-spec-kit-an-honest-field-report/)
|
||||
- **Particula Tech** (Sebastian Mondragon, June 18) compared Spec Kit, Kiro, and Tessl, calling Spec Kit the heaviest and most flexible (30+ agents) but "prone to review overload" — match tool weight to task. [\[particula.tech\]](https://particula.tech/blog/spec-driven-development-tools-spec-kit-vs-kiro-vs-tessl)
|
||||
- **ToolTwist** (Portia Canlas, June 10) published a CxO field guide to BMAD, OpenSpec, and Spec Kit, concluding "none is best" and calling Spec Kit the **safe default for scaling teams**. [\[tooltwist.com\]](https://tooltwist.com/insights/spec-driven-frameworks-cxo-guide)
|
||||
- **Allegro Tech** (Konrad Piechna, June 8) shared hard-won SDD best practices, threading Spec Kit's Specify→Plan→Implement→Validate model throughout. [\[blog.allegro.tech\]](https://blog.allegro.tech/2026/06/spec-driven-development-best-practices.html)
|
||||
- **Yauhen Pyl** (June 3) published a hands-on scoring comparison rating Spec-Kit 2.77 vs OpenSpec 4.00 for brownfield/DX — praising the constitution model while calling it verbose and greenfield-biased. [\[ypyl.github.io\]](https://ypyl.github.io/programming/2026/06/03/openspec-vs-spec-kit-sdd.html)
|
||||
|
||||
Notable non-English coverage:
|
||||
|
||||
- **Japanese**: haru_iida published a thorough install + `/speckit.*` tutorial on Zenn from 6+ months of use. [\[zenn.dev\]](https://zenn.dev/haru_iida/articles/github-spec-kit-guide) A Qiita piece by IBM's Tomoyuki Hori documented integrating Spec Kit into the IBM Bob IDE. [\[qiita.com\]](https://qiita.com/Tomoyuki_Hori/items/eb0b1db560ba804cf8ac)
|
||||
- **Chinese**: 掘金 (juejin.cn) ran multiple three-way "Spec Kit vs OpenSpec vs Superpowers" decision guides, and 腾讯云 published a balanced "spec as scaffolding vs single truth" analysis. [\[juejin.cn\]](https://juejin.cn/post/7657070407262421007)
|
||||
- **Korean**: velog and Naver carried a wave of hands-on build logs and honest "is it too heavy?" critiques, including a full Claude Code + Spec-Kit end-to-end build. [\[velog.io\]](https://velog.io/@yono/GitHub-Spec-Kit%EC%9C%BC%EB%A1%9C-Spec-Driven-Development-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0)
|
||||
- **Russian**: a vc.ru field report trialed Spec Kit across four projects, concluding roughly 30% of the author's work suits it — strong on greenfield, weak on research and existing code. [\[vc.ru\]](https://vc.ru/ai/2974391-opyt-ispolzovaniya-spec-kit-na-proyektakh)
|
||||
|
||||
Coverage also appeared on TabNews (Portuguese), Habr and CSDN, note.com, Substack (multiple), Medium, DEV Community, Design News, and company engineering blogs — the broadest linguistic spread yet recorded.
|
||||
|
||||
### Community Growth by the Numbers
|
||||
|
||||
| Metric | Start of June | End of June | Change |
|
||||
| --- | --- | --- | --- |
|
||||
| GitHub stars | 106,951 | ~116,500 | +~9,500 (+9%) |
|
||||
| Forks | 9,464 | ~10,250 | +~800 |
|
||||
| Contributors | 217 | 245 | +28 |
|
||||
| Releases (total) | 152 | 177 | +25 (v0.9.0–v0.12.2) |
|
||||
| Community extensions | 105 | 124 | +19 |
|
||||
| Community presets | 21 | 23 | +2 |
|
||||
| Discussions (open) | 422 | 436 | +14 |
|
||||
|
||||
## SDD Ecosystem & Industry Trends
|
||||
|
||||
### The Category Consolidates
|
||||
|
||||
Across June's record article volume, a consistent framing emerged: spec-driven development is now an established category, and Spec Kit is its reference implementation. SSOJet called it "the category anchor," Design News and multiple comparison pieces called it the agent-neutral default, and ToolTwist's CxO guide named it the "safe default for scaling teams." The Microsoft Developer Blog post and the SNCF enterprise interview extended that framing beyond the developer press into institutional and enterprise contexts. [\[ssojet.com\]](https://ssojet.com/blog/best-spec-driven-development-tools)
|
||||
|
||||
### Competitive Landscape
|
||||
|
||||
The "which SDD tool?" comparison became June's dominant content genre, almost always featuring the same field: **Spec Kit, OpenSpec, Superpowers, BMAD, Kiro, Tessl, and GSD**. The recurring conclusion — from ToolTwist, BrainGrid, Particula Tech, and numerous multilingual surveys — was that the *practice* matters more than the tool, with Spec Kit positioned as the portable, community-driven, agent-agnostic default and competitors differentiating on brownfield ergonomics and drift management. Balanced reviews were consistent about the trade-off: Spec Kit is the heaviest and most flexible option (30+ agents, a full constitution/lifecycle model), which brings both the widest capability surface and the most review overhead. Hands-on scoring pieces (ypyl, vc.ru) rated it strong on greenfield and multi-scenario work and weaker on research tasks and incremental brownfield edits — precisely the gaps the `/speckit.converge` loop and the growing brownfield/drift extension ecosystem are built to close. [\[tooltwist.com\]](https://tooltwist.com/insights/spec-driven-frameworks-cxo-guide)
|
||||
|
||||
## Roadmap
|
||||
|
||||
Areas under discussion or in progress for future development:
|
||||
|
||||
- **The convergence loop** — `/speckit.converge` (#3001) is the core's direct answer to the drift-and-verification concern raised in nearly every review. Expect the append-only convergence model to deepen, and the community drift/verification extensions (Golden Demo, Spec Trace, Coding Standards Drift Control) to keep feeding requirements upstream. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/quickstart.md)
|
||||
- **The bundle subsystem** — `specify bundle` (#3070) establishes role-based distribution as a first-class primitive. With a community submission path now open (#3162) and four example roles shipped, curation, trust signals (`verified` vs `community`), and version-pin enforcement become the next areas to mature. [\[github.com\]](https://github.com/github/spec-kit/blob/main/docs/reference/bundles.md)
|
||||
- **A programmable workflow platform** — with the step catalog, JSON output, and `from_json` filter, workflows are now community-extensible and scriptable. The open question is discoverability and pull: the step catalog is new, and adoption will show whether standalone workflow authoring becomes a real ecosystem or stays a power-user niche. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **PyPI publishing** — a publishing workflow and README metadata landed (#2915, closing #2623), but official PyPI distribution is not yet the recommended install path; `uv tool install` and git remain canonical. Completing and hardening this reduces friction for restricted/air-gapped environments. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **CLI architecture cleanup** — the `__init__.py` decomposition reached 7/8 (extensions/_commands.py, #3014), with one part remaining. The payoff is contributor onboarding and test isolation. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **Toward a stable release** — v0.10.0's removal of `--no-git` and the git extension going opt-in was the first real breaking change, and the run to v0.12 reflects sustained pre-1.0 momentum. Expect continued API stabilization as the surface (bundles, workflows, converge) settles. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **Experience simplification** — review overload, ceremony for small tasks, and verbose markdown output remain the most-cited concerns across June's balanced reviews (Particula Tech, ypyl, vc.ru, multiple Korean and Japanese pieces). The lean preset, TinySpec, `/speckit.converge`, and role bundles provide answers; surfacing them to new users is the ongoing opportunity. [\[particula.tech\]](https://particula.tech/blog/spec-driven-development-tools-spec-kit-vs-kiro-vs-tessl)
|
||||
@@ -99,7 +99,7 @@ The `CommandRegistrar` renders commands differently per agent:
|
||||
|
||||
| Agent | Format | Extension | Arg placeholder |
|
||||
|-------|--------|-----------|-----------------|
|
||||
| Claude, Kilo Code, opencode, etc. | Markdown | `.md` | `$ARGUMENTS` |
|
||||
| Claude, Cursor, opencode, Windsurf, etc. | Markdown | `.md` | `$ARGUMENTS` |
|
||||
| Copilot | Markdown | `.agent.md` + `.prompt.md` | `$ARGUMENTS` |
|
||||
| Gemini, Qwen, Tabnine | TOML | `.toml` | `{{args}}` |
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Before publishing a preset, ensure you have:
|
||||
|
||||
1. **Valid Preset**: A working preset with a valid `preset.yml` manifest
|
||||
2. **Git Repository**: Preset hosted on GitHub (or other public git hosting)
|
||||
3. **Documentation**: A preset-scoped README.md that explains how to use **this preset**, including a valid `specify preset add ...` install command (see [Usage README Requirements](#usage-readme-requirements))
|
||||
3. **Documentation**: README.md with description and usage instructions
|
||||
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
|
||||
5. **Versioning**: Semantic versioning (e.g., 1.0.0)
|
||||
6. **Testing**: Preset tested on real projects with `specify preset add --dev`
|
||||
@@ -147,46 +147,6 @@ https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0
|
||||
specify preset add --from https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip
|
||||
```
|
||||
|
||||
### Usage README Requirements
|
||||
|
||||
The catalog `documentation` field must point at a README that explains how to use
|
||||
**this preset** — not a product pitch for a broader framework or a separate CLI.
|
||||
|
||||
The submission workflow **mechanically enforces** that the linked README is a GitHub-hosted
|
||||
URL whose path ends with `README.md`, resolves to a readable file, and contains at least one
|
||||
valid `specify preset add ...` command. The remaining items (preferring a preset-scoped README
|
||||
in monorepos, covering the minimum structure) are expectations a human reviewer checks —
|
||||
follow them so your submission isn't sent back for changes.
|
||||
|
||||
- **Point `documentation` at the preset-scoped README.** In a monorepo where the preset
|
||||
lives in a subdirectory (e.g. `presets/<id>/`), link the README inside that directory
|
||||
(`presets/<id>/README.md`) rather than the repository-root README. The root README is
|
||||
often a marketing/overview page; the catalog should surface preset usage instead. The key
|
||||
requirement is that this README is reachable at the `documentation` URL so users can read
|
||||
it *before* downloading the release artifact — it's fine for the same file to also ship
|
||||
inside the release ZIP.
|
||||
- **Include a valid Spec Kit CLI install command** *(enforced)*. The linked README must
|
||||
contain at least one `specify preset add ...` invocation. Preferably use the
|
||||
catalog-install form whose URL matches your Download URL:
|
||||
|
||||
```bash
|
||||
# <download-url> is the same URL you submit as the catalog Download URL —
|
||||
# either the tag archive or a release asset, e.g.:
|
||||
specify preset add --from https://github.com/<org>/<repo>/archive/refs/tags/vX.Y.Z.zip
|
||||
specify preset add --from https://github.com/<org>/<repo>/releases/download/vX.Y.Z/<id>-X.Y.Z.zip
|
||||
```
|
||||
|
||||
`specify preset add <id>` and `specify preset add --dev <path>` are also accepted, but the
|
||||
`--from <download-url>` form is the clearest signal that the README documents this exact
|
||||
preset release.
|
||||
- **Cover the minimum structure** so a reader can decide whether the preset fits:
|
||||
- What the preset does / what it provides
|
||||
- The install command using Spec Kit CLI syntax (above)
|
||||
- When to use it / when not to use it
|
||||
|
||||
A submission whose linked README lacks a valid `specify preset add ...` command **fails
|
||||
validation** (workflow check 2d) and will not be added until corrected.
|
||||
|
||||
---
|
||||
|
||||
## Submit to Catalog
|
||||
@@ -221,14 +181,11 @@ Edit `presets/catalog.community.json` and add your preset.
|
||||
"presets": {
|
||||
"your-preset": {
|
||||
"name": "Your Preset Name",
|
||||
"id": "your-preset",
|
||||
"description": "Brief description of what your preset provides",
|
||||
"author": "Your Name",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/your-org/spec-kit-preset-your-preset/archive/refs/tags/v1.0.0.zip",
|
||||
"sha256": "OPTIONAL: SHA-256 hex digest of the archive above; verified before install",
|
||||
"repository": "https://github.com/your-org/spec-kit-preset-your-preset",
|
||||
"documentation": "https://github.com/your-org/spec-kit-preset-your-preset/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0"
|
||||
@@ -285,7 +242,7 @@ git push origin add-your-preset
|
||||
|
||||
### Checklist
|
||||
- [ ] Valid preset.yml manifest
|
||||
- [ ] Usage README with a valid `specify preset add ...` command, linked from `documentation` (preset-scoped README recommended for monorepos)
|
||||
- [ ] README.md with description and usage
|
||||
- [ ] LICENSE file included
|
||||
- [ ] GitHub release created
|
||||
- [ ] Preset tested with `specify preset add --dev`
|
||||
@@ -306,15 +263,7 @@ After submission, maintainers will review:
|
||||
2. **Template quality** — templates are useful and well-structured
|
||||
3. **Command coherence** — commands reference sections that exist in templates
|
||||
4. **Security** — no malicious content, safe file operations
|
||||
5. **Documentation** — the README linked from `documentation` explains how to use *this* preset and contains a valid `specify preset add ...` command
|
||||
|
||||
> **Reviewer note:** the workflow can mechanically check *structure* (the linked README
|
||||
> resolves and contains a valid `specify preset add ...` snippet; when that snippet uses the
|
||||
> `--from <url>` form, its URL must match the submitted download URL exactly — other accepted
|
||||
> forms like `specify preset add <id>` don't reference the download URL at all). Whether the
|
||||
> README genuinely documents *this* preset is partly a content judgment, so a human reviewer
|
||||
> should still confirm the linked doc isn't just a funnel to a separate product or CLI before
|
||||
> approving.
|
||||
5. **Documentation** — clear README explaining what the preset does
|
||||
|
||||
Once verified, `verified: true` is set and the preset appears in `specify preset search`.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-06-30T00:00:00Z",
|
||||
"updated_at": "2026-06-22T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
||||
"presets": {
|
||||
"a11y-governance": {
|
||||
@@ -567,13 +567,13 @@
|
||||
"sicario-core": {
|
||||
"name": "SicarioSpec Core",
|
||||
"id": "sicario-core",
|
||||
"version": "0.5.1",
|
||||
"description": "Baseline secure-by-default Spec Kit governance profile.",
|
||||
"version": "0.4.0",
|
||||
"description": "Evidence-first security operations governance that maps feature risk to controls, gates, evidence, owners, approval, and accepted-risk decisions.",
|
||||
"author": "SicarioSpec Contributors",
|
||||
"repository": "https://github.com/dfirs1car1o/sicario-spec",
|
||||
"download_url": "https://github.com/dfirs1car1o/sicario-spec/releases/download/v0.5.1/sicario-core-0.5.1.zip",
|
||||
"download_url": "https://github.com/dfirs1car1o/sicario-spec/releases/download/v0.4.0/sicario-core-0.4.0.zip",
|
||||
"homepage": "https://github.com/dfirs1car1o/sicario-spec",
|
||||
"documentation": "https://github.com/dfirs1car1o/sicario-spec/blob/main/presets/sicario-core/README.md",
|
||||
"documentation": "https://github.com/dfirs1car1o/sicario-spec/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.9.0"
|
||||
@@ -583,13 +583,14 @@
|
||||
"commands": 0
|
||||
},
|
||||
"tags": [
|
||||
"security",
|
||||
"governance",
|
||||
"security-ops",
|
||||
"secure-by-default",
|
||||
"evidence"
|
||||
],
|
||||
"created_at": "2026-06-22T00:00:00Z",
|
||||
"updated_at": "2026-06-25T00:00:00Z"
|
||||
"updated_at": "2026-06-22T00:00:00Z"
|
||||
},
|
||||
"spec2cloud": {
|
||||
"name": "Spec2Cloud",
|
||||
@@ -670,11 +671,11 @@
|
||||
"workflow-preset": {
|
||||
"name": "Workflow Preset",
|
||||
"id": "workflow-preset",
|
||||
"version": "1.3.11",
|
||||
"description": "Behavior-first specification, design artifacts, and agent-native handoff orchestration",
|
||||
"version": "1.3.2",
|
||||
"description": "Behavior-first specification, design artifacts, and agent-native handoff orchestration.",
|
||||
"author": "bigsmartben",
|
||||
"repository": "https://github.com/bigsmartben/spec-kit-workflow-preset",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-workflow-preset/releases/download/v1.3.11/spec-kit-workflow-preset-v1.3.11.zip",
|
||||
"download_url": "https://github.com/bigsmartben/spec-kit-workflow-preset/releases/download/v1.3.2/spec-kit-workflow-preset-v1.3.2.zip",
|
||||
"homepage": "https://github.com/bigsmartben/spec-kit-workflow-preset",
|
||||
"documentation": "https://github.com/bigsmartben/spec-kit-workflow-preset/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
@@ -693,7 +694,7 @@
|
||||
"handoff"
|
||||
],
|
||||
"created_at": "2026-05-27T00:00:00Z",
|
||||
"updated_at": "2026-06-30T00:00:00Z"
|
||||
"updated_at": "2026-06-03T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.12.3"
|
||||
version = "0.11.6"
|
||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
@@ -74,13 +74,3 @@ precision = 2
|
||||
show_missing = true
|
||||
skip_covered = false
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Lock in subprocess security posture: any reintroduction of shell=True
|
||||
# (or os.system / popen2) must be acknowledged with an explicit `# noqa`
|
||||
# pointing at the rule, making the deviation visible in review.
|
||||
extend-select = [
|
||||
"S602", # subprocess-popen-with-shell-equals-true
|
||||
"S604", # call-with-shell-equals-true
|
||||
"S605", # start-process-with-a-shell
|
||||
]
|
||||
|
||||
|
||||
@@ -78,14 +78,8 @@ done
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
# Get feature paths.
|
||||
# In --paths-only mode this is pure resolution, so pass --no-persist to opt out
|
||||
# of the feature.json write side effect (issue #3025).
|
||||
if $PATHS_ONLY; then
|
||||
_paths_output=$(get_feature_paths --no-persist) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||
else
|
||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||
fi
|
||||
# Get feature paths
|
||||
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
|
||||
eval "$_paths_output"
|
||||
unset _paths_output
|
||||
|
||||
|
||||
@@ -152,15 +152,6 @@ _persist_feature_json() {
|
||||
}
|
||||
|
||||
get_feature_paths() {
|
||||
# Read-only callers (e.g. check-prerequisites.sh --paths-only) pass
|
||||
# --no-persist so pure path resolution never writes .specify/feature.json,
|
||||
# which would dirty the working tree or overwrite a pinned value (issue #3025).
|
||||
local no_persist=false
|
||||
if [[ "${1:-}" == "--no-persist" ]]; then
|
||||
no_persist=true
|
||||
shift
|
||||
fi
|
||||
|
||||
# Split decl/assignment so a SPECIFY_INIT_DIR validation failure in
|
||||
# get_repo_root propagates as a hard error instead of being masked by `local`.
|
||||
local repo_root
|
||||
@@ -177,11 +168,8 @@ get_feature_paths() {
|
||||
feature_dir="$SPECIFY_FEATURE_DIRECTORY"
|
||||
# Normalize relative paths to absolute under repo root
|
||||
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
|
||||
# Persist to feature.json so future sessions without the env var still
|
||||
# work — unless the caller opted out for read-only resolution (#3025).
|
||||
if [[ "$no_persist" != true ]]; then
|
||||
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
|
||||
fi
|
||||
# Persist to feature.json so future sessions without the env var still work
|
||||
_persist_feature_json "$repo_root" "$SPECIFY_FEATURE_DIRECTORY"
|
||||
elif [[ -f "$repo_root/.specify/feature.json" ]]; then
|
||||
local _fd
|
||||
_fd=$(read_feature_json_feature_directory "$repo_root")
|
||||
|
||||
@@ -140,7 +140,7 @@ generate_branch_name() {
|
||||
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=$(printf '%s' "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
||||
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=()
|
||||
@@ -152,10 +152,8 @@ generate_branch_name() {
|
||||
if ! echo "$word" | grep -qiE "$stop_words"; then
|
||||
if [ ${#word} -ge 3 ]; then
|
||||
meaningful_words+=("$word")
|
||||
# Keep short words that appear as an uppercase acronym in the original.
|
||||
# Uppercase via tr and match with grep -w (both portable) rather than
|
||||
# bash's 4+ "^^" case expansion (breaks on macOS bash 3.2) and \b (non-POSIX).
|
||||
elif printf '%s' "$description" | grep -qw -- "$(printf '%s' "$word" | tr '[:lower:]' '[:upper:]')"; then
|
||||
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
|
||||
|
||||
@@ -56,14 +56,8 @@ EXAMPLES:
|
||||
# Source common functions
|
||||
. "$PSScriptRoot/common.ps1"
|
||||
|
||||
# Get feature paths.
|
||||
# In -PathsOnly mode this is pure resolution, so pass -NoPersist to opt out of
|
||||
# the feature.json write side effect (issue #3025).
|
||||
if ($PathsOnly) {
|
||||
$paths = Get-FeaturePathsEnv -NoPersist
|
||||
} else {
|
||||
$paths = Get-FeaturePathsEnv
|
||||
}
|
||||
# Get feature paths
|
||||
$paths = Get-FeaturePathsEnv
|
||||
|
||||
# If paths-only mode, output paths and exit (no validation)
|
||||
if ($PathsOnly) {
|
||||
@@ -89,24 +83,24 @@ if ($PathsOnly) {
|
||||
|
||||
# Validate required directories and files
|
||||
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
|
||||
[Console]::Error.WriteLine("ERROR: Feature directory not found: $($paths.FEATURE_DIR)")
|
||||
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
|
||||
$specifyCommand = Format-SpecKitCommand -CommandName 'specify' -RepoRoot $paths.REPO_ROOT
|
||||
[Console]::Error.WriteLine("Run $specifyCommand first to create the feature structure.")
|
||||
Write-Output "Run $specifyCommand first to create the feature structure."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
|
||||
[Console]::Error.WriteLine("ERROR: plan.md not found in $($paths.FEATURE_DIR)")
|
||||
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
|
||||
$planCommand = Format-SpecKitCommand -CommandName 'plan' -RepoRoot $paths.REPO_ROOT
|
||||
[Console]::Error.WriteLine("Run $planCommand first to create the implementation plan.")
|
||||
Write-Output "Run $planCommand first to create the implementation plan."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check for tasks.md if required
|
||||
if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) {
|
||||
[Console]::Error.WriteLine("ERROR: tasks.md not found in $($paths.FEATURE_DIR)")
|
||||
Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)"
|
||||
$tasksCommand = Format-SpecKitCommand -CommandName 'tasks' -RepoRoot $paths.REPO_ROOT
|
||||
[Console]::Error.WriteLine("Run $tasksCommand first to create the task list.")
|
||||
Write-Output "Run $tasksCommand first to create the task list."
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
@@ -143,13 +143,6 @@ function Save-FeatureJson {
|
||||
}
|
||||
|
||||
function Get-FeaturePathsEnv {
|
||||
# Read-only callers (e.g. check-prerequisites.ps1 -PathsOnly) pass -NoPersist
|
||||
# so pure path resolution never writes .specify/feature.json, which would
|
||||
# dirty the working tree or overwrite a pinned value (issue #3025).
|
||||
param(
|
||||
[switch]$NoPersist
|
||||
)
|
||||
|
||||
$repoRoot = Get-RepoRoot
|
||||
$currentBranch = Get-CurrentBranch
|
||||
|
||||
@@ -164,11 +157,8 @@ function Get-FeaturePathsEnv {
|
||||
if (-not [System.IO.Path]::IsPathRooted($featureDir)) {
|
||||
$featureDir = Join-Path $repoRoot $featureDir
|
||||
}
|
||||
# Persist to feature.json so future sessions without the env var still
|
||||
# work - unless the caller opted out for read-only resolution (#3025).
|
||||
if (-not $NoPersist) {
|
||||
Save-FeatureJson -RepoRoot $repoRoot -FeatureDirectory $env:SPECIFY_FEATURE_DIRECTORY
|
||||
}
|
||||
# Persist to feature.json so future sessions without the env var still work
|
||||
Save-FeatureJson -RepoRoot $repoRoot -FeatureDirectory $env:SPECIFY_FEATURE_DIRECTORY
|
||||
} elseif (Test-Path $featureJson) {
|
||||
$featureJsonRaw = Get-Content -LiteralPath $featureJson -Raw
|
||||
try {
|
||||
@@ -219,13 +209,7 @@ function Test-FileExists {
|
||||
|
||||
function Test-DirHasFiles {
|
||||
param([string]$Path, [string]$Description)
|
||||
# A directory counts as non-empty when Get-ChildItem returns any entry
|
||||
# (files or subdirectories) -- matching the JSON contracts checks in
|
||||
# check-prerequisites.ps1 / setup-tasks.ps1, and treating a directory whose
|
||||
# only contents are subdirectories (e.g. contracts/v1/openapi.yaml) as
|
||||
# non-empty like bash check_dir. Filtering out subdirectories would
|
||||
# mis-report such a directory as empty.
|
||||
if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Select-Object -First 1)) {
|
||||
if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) {
|
||||
Write-Output " [OK] $Description"
|
||||
return $true
|
||||
} else {
|
||||
|
||||
@@ -111,11 +111,8 @@ function Get-BranchName {
|
||||
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
|
||||
if ($word.Length -ge 3) {
|
||||
$meaningfulWords += $word
|
||||
} elseif ($Description -cmatch "\b$($word.ToUpper())\b") {
|
||||
# Keep short words only if they appear as uppercase in original (likely
|
||||
# acronyms). Use -cmatch so the comparison is case-sensitive, matching the
|
||||
# bash script's case-sensitive grep; -match would be case-insensitive and
|
||||
# would keep every short word.
|
||||
} elseif ($Description -match "\b$($word.ToUpper())\b") {
|
||||
# Keep short words if they appear as uppercase in original (likely acronyms)
|
||||
$meaningfulWords += $word
|
||||
}
|
||||
}
|
||||
@@ -142,10 +139,8 @@ if ($ShortName) {
|
||||
$branchSuffix = Get-BranchName -Description $featureDesc
|
||||
}
|
||||
|
||||
# Warn if -Number and -Timestamp are both specified. Use ContainsKey (not
|
||||
# `-ne 0`) so an explicit `-Number 0` is also detected, matching the bash twin's
|
||||
# `[ -n "$BRANCH_NUMBER" ]` check.
|
||||
if ($Timestamp -and $PSBoundParameters.ContainsKey('Number')) {
|
||||
# Warn if -Number and -Timestamp are both specified
|
||||
if ($Timestamp -and $Number -ne 0) {
|
||||
Write-Warning "[specify] Warning: -Number is ignored when -Timestamp is used"
|
||||
$Number = 0
|
||||
}
|
||||
@@ -155,10 +150,8 @@ if ($Timestamp) {
|
||||
$featureNum = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$branchName = "$featureNum-$branchSuffix"
|
||||
} else {
|
||||
# Determine branch number from existing feature directories. Auto-detect only
|
||||
# when -Number was not supplied; an explicit value (including 0) is honored,
|
||||
# matching the bash twin's `[ -z "$BRANCH_NUMBER" ]` check.
|
||||
if (-not $PSBoundParameters.ContainsKey('Number')) {
|
||||
# Determine branch number from existing feature directories
|
||||
if ($Number -eq 0) {
|
||||
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
||||
}
|
||||
|
||||
@@ -211,10 +204,6 @@ if (-not $DryRun) {
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($specFile, $content, $utf8NoBom)
|
||||
} else {
|
||||
# Match the bash twin (create-new-feature.sh): warn on stderr that no
|
||||
# spec template was found before creating an empty spec file, so the
|
||||
# missing-template signal is not silently swallowed on Windows.
|
||||
[Console]::Error.WriteLine("Warning: Spec template not found; created empty spec file")
|
||||
New-Item -ItemType File -Path $specFile -Force | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,22 +40,8 @@ if (Test-Path $paths.IMPL_PLAN -PathType Leaf) {
|
||||
$content = [System.IO.File]::ReadAllText($template)
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($paths.IMPL_PLAN, $content, $utf8NoBom)
|
||||
# Emit the copy status like the bash twin (setup-plan.sh); route to stderr
|
||||
# in -Json mode so stdout stays pure JSON, matching the sibling messages.
|
||||
if ($Json) {
|
||||
[Console]::Error.WriteLine("Copied plan template to $($paths.IMPL_PLAN)")
|
||||
} else {
|
||||
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
|
||||
}
|
||||
} else {
|
||||
# Match the bash twin's wording and stream routing (stderr in -Json so
|
||||
# stdout stays pure JSON, stdout otherwise), consistent with the sibling
|
||||
# "Copied plan template" message above.
|
||||
if ($Json) {
|
||||
[Console]::Error.WriteLine("Warning: Plan template not found")
|
||||
} else {
|
||||
Write-Output "Warning: Plan template not found"
|
||||
}
|
||||
Write-Warning "Plan template not found"
|
||||
# Create a basic plan file if template doesn't exist
|
||||
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,10 +34,6 @@ TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit"
|
||||
|
||||
console = Console(highlight=False)
|
||||
|
||||
# Stderr-bound console for error/diagnostic output, so human-facing messages
|
||||
# never contaminate stdout (which carries machine-readable ``--json`` payloads).
|
||||
err_console = Console(stderr=True, highlight=False)
|
||||
|
||||
class StepTracker:
|
||||
"""Track and render hierarchical steps without emojis, similar to Claude Code tree output.
|
||||
Supports live auto-refresh via an attached refresh callback.
|
||||
|
||||
@@ -10,7 +10,6 @@ through the config-driven helpers in :mod:`specify_cli.authentication.http`.
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
from fnmatch import fnmatch
|
||||
from typing import Callable, Dict, Optional
|
||||
from urllib.parse import quote, unquote, urlparse
|
||||
|
||||
@@ -57,79 +56,55 @@ def build_github_request(url: str) -> urllib.request.Request:
|
||||
return urllib.request.Request(url, headers=headers)
|
||||
|
||||
|
||||
def _host_matches(hostname: str, patterns: tuple[str, ...]) -> bool:
|
||||
"""Return True when *hostname* matches a pattern (exact or ``*.suffix``)."""
|
||||
hostname = hostname.lower()
|
||||
return any(p == hostname or fnmatch(hostname, p) for p in patterns)
|
||||
|
||||
|
||||
def resolve_github_release_asset_api_url(
|
||||
download_url: str,
|
||||
open_url_fn: Callable,
|
||||
timeout: int = 60,
|
||||
github_hosts: tuple[str, ...] = (),
|
||||
) -> Optional[str]:
|
||||
"""Resolve a GitHub release browser-download URL to its REST API asset URL.
|
||||
"""Resolve a GitHub browser release URL to its REST API asset URL.
|
||||
|
||||
Works for public ``github.com`` and for GitHub Enterprise Server (GHES)
|
||||
hosts. A host is treated as GHES when it matches one of *github_hosts*
|
||||
(exact hostname or ``*.suffix``) — supply the hosts the user has trusted
|
||||
under a ``github`` provider in ``auth.json``. This allowlist is the
|
||||
security gate: unlisted hosts never receive GHES API treatment, so a
|
||||
malicious catalog cannot induce an API request to an arbitrary host.
|
||||
For private or SSO-protected repositories, browser release download
|
||||
URLs (``https://github.com/<owner>/<repo>/releases/download/<tag>/<asset>``)
|
||||
redirect to an HTML/SSO page instead of delivering the file. This
|
||||
helper resolves such a URL to the matching GitHub REST API asset URL
|
||||
(``https://api.github.com/repos/…/releases/assets/<id>``), which can
|
||||
then be downloaded with ``Accept: application/octet-stream`` and an
|
||||
auth token to retrieve the actual file payload.
|
||||
|
||||
For a public URL the API base is ``https://api.github.com``; for a GHES
|
||||
host it is ``{scheme}://{host[:port]}/api/v3``. Returns the API asset URL
|
||||
(downloadable with ``Accept: application/octet-stream`` + a token), the
|
||||
input unchanged if it is already an API asset URL, or ``None`` when the
|
||||
URL is not a resolvable GitHub release download or the lookup fails.
|
||||
If *download_url* is already a REST API asset URL, it is returned
|
||||
as-is. Non-GitHub URLs and GitHub URLs that are not release-download
|
||||
URLs return ``None``. If the API lookup fails (e.g. network error or
|
||||
asset not found), ``None`` is returned so callers can fall back to the
|
||||
original URL.
|
||||
|
||||
Args:
|
||||
download_url: The URL to resolve.
|
||||
open_url_fn: A callable compatible with
|
||||
``specify_cli.authentication.http.open_url`` used for the
|
||||
authenticated release-metadata lookup.
|
||||
``specify_cli.authentication.http.open_url`` used to make the
|
||||
authenticated API request.
|
||||
timeout: Per-request timeout in seconds.
|
||||
github_hosts: Host patterns to treat as GitHub Enterprise Server.
|
||||
|
||||
Returns:
|
||||
The resolved REST API asset URL, or ``None`` if resolution is not
|
||||
applicable or fails.
|
||||
"""
|
||||
import json
|
||||
import urllib.error
|
||||
|
||||
parsed = urlparse(download_url)
|
||||
hostname = (parsed.hostname or "").lower()
|
||||
parts = [unquote(part) for part in parsed.path.strip("/").split("/")]
|
||||
|
||||
is_ghes = (
|
||||
bool(hostname)
|
||||
and hostname not in GITHUB_HOSTS
|
||||
and _host_matches(hostname, github_hosts)
|
||||
)
|
||||
|
||||
def _is_asset_path(segments: list[str]) -> bool:
|
||||
return (
|
||||
len(segments) >= 6
|
||||
and segments[:1] == ["repos"]
|
||||
and segments[3:5] == ["releases", "assets"]
|
||||
)
|
||||
|
||||
# Already a REST API asset URL — use it directly. Pure passthrough induces
|
||||
# no new request: the caller fetches this same URL regardless, so it is
|
||||
# gated on path shape alone rather than the GHES allowlist. The token stays
|
||||
# independently gated by auth.json in the download helper, and only the
|
||||
# resolving path below (which issues a tag-lookup request) needs the
|
||||
# allowlist as its anti-SSRF gate.
|
||||
if hostname == "api.github.com" and _is_asset_path(parts):
|
||||
return download_url
|
||||
if hostname and parts[:2] == ["api", "v3"] and _is_asset_path(parts[2:]):
|
||||
# Already a REST API asset URL — use it directly
|
||||
if (
|
||||
parsed.hostname == "api.github.com"
|
||||
and len(parts) >= 6
|
||||
and parts[:1] == ["repos"]
|
||||
and parts[3:5] == ["releases", "assets"]
|
||||
):
|
||||
return download_url
|
||||
|
||||
# Determine the REST API base for browser release-download URLs.
|
||||
if hostname == "github.com":
|
||||
api_base = "https://api.github.com"
|
||||
elif is_ghes:
|
||||
authority = hostname if parsed.port is None else f"{hostname}:{parsed.port}"
|
||||
api_base = f"{parsed.scheme}://{authority}/api/v3"
|
||||
else:
|
||||
# Only handle github.com browser release download URLs
|
||||
if parsed.hostname != "github.com":
|
||||
return None
|
||||
|
||||
# Expecting /<owner>/<repo>/releases/download/<tag>/<asset>
|
||||
@@ -139,7 +114,7 @@ def resolve_github_release_asset_api_url(
|
||||
owner, repo, tag = parts[0], parts[1], parts[4]
|
||||
asset_name = "/".join(parts[5:])
|
||||
encoded_tag = quote(tag, safe="")
|
||||
release_url = f"{api_base}/repos/{owner}/{repo}/releases/tags/{encoded_tag}"
|
||||
release_url = f"https://api.github.com/repos/{owner}/{repo}/releases/tags/{encoded_tag}"
|
||||
|
||||
try:
|
||||
with open_url_fn(release_url, timeout=timeout) as response:
|
||||
|
||||
@@ -65,31 +65,14 @@ def dump_frontmatter(data: dict[str, Any]) -> str:
|
||||
return yaml.safe_dump(data, sort_keys=False, allow_unicode=True).strip()
|
||||
|
||||
|
||||
def run_command(
|
||||
cmd: list[str],
|
||||
check_return: bool = True,
|
||||
capture: bool = False,
|
||||
shell: bool = False,
|
||||
) -> str | None:
|
||||
"""Run a command without invoking a shell and optionally capture output.
|
||||
|
||||
The ``shell`` parameter is kept in the signature so existing keyword
|
||||
callers (and the re-export from ``specify_cli``) don't raise ``TypeError``,
|
||||
but only the default ``shell=False`` is honoured. ``shell=True`` is
|
||||
rejected with ``ValueError`` rather than silently ignored, so the
|
||||
unsupported mode fails loudly instead of running with a different meaning.
|
||||
"""
|
||||
if shell:
|
||||
raise ValueError(
|
||||
"run_command() does not support shell=True; pass argv as a list"
|
||||
)
|
||||
|
||||
def run_command(cmd: list[str], check_return: bool = True, capture: bool = False, shell: bool = False) -> str | None:
|
||||
"""Run a shell command and optionally capture output."""
|
||||
try:
|
||||
if capture:
|
||||
result = subprocess.run(cmd, check=check_return, capture_output=True, text=True)
|
||||
result = subprocess.run(cmd, check=check_return, capture_output=True, text=True, shell=shell)
|
||||
return result.stdout.strip()
|
||||
else:
|
||||
subprocess.run(cmd, check=check_return)
|
||||
subprocess.run(cmd, check=check_return, shell=shell)
|
||||
return None
|
||||
except subprocess.CalledProcessError as e:
|
||||
if check_return:
|
||||
@@ -304,27 +287,3 @@ def _display_project_path(project_root: Path, path: str | Path) -> str:
|
||||
except (OSError, ValueError):
|
||||
return path_obj.as_posix()
|
||||
return rel_path.as_posix()
|
||||
|
||||
|
||||
def version_satisfies(current: str, required: str) -> bool:
|
||||
"""Check if current version satisfies required version specifier.
|
||||
|
||||
Evaluates the version against the specifier using the project's
|
||||
prerelease policy (prereleases are allowed).
|
||||
|
||||
Args:
|
||||
current: Current version (e.g., "0.1.5")
|
||||
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")
|
||||
|
||||
Returns:
|
||||
True if version satisfies requirement
|
||||
"""
|
||||
from packaging import version as pkg_version
|
||||
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
||||
|
||||
try:
|
||||
current_ver = pkg_version.Version(current)
|
||||
specifier = SpecifierSet(required)
|
||||
return specifier.contains(current_ver, prereleases=True)
|
||||
except (pkg_version.InvalidVersion, InvalidSpecifier):
|
||||
return False
|
||||
|
||||
@@ -37,8 +37,6 @@ def _build_agent_configs() -> dict[str, Any]:
|
||||
# when register_commands() resolves __SPECKIT_COMMAND_*__ tokens.
|
||||
if "invoke_separator" not in config:
|
||||
config["invoke_separator"] = integration.invoke_separator
|
||||
if integration.dev_no_symlink:
|
||||
config["dev_no_symlink"] = True
|
||||
configs[key] = config
|
||||
return configs
|
||||
|
||||
@@ -236,14 +234,9 @@ class CommandRegistrar:
|
||||
toml_lines.append(f"# Source: {source_id}")
|
||||
toml_lines.append("")
|
||||
|
||||
# Keep TOML output valid even when body contains triple-quote delimiters
|
||||
# or backslashes. Prefer multiline forms, then fall back to escaped basic
|
||||
# string. A multiline *basic* string ("""...""") processes backslash escape
|
||||
# sequences, so a body containing a backslash (e.g. a Windows path
|
||||
# ``C:\\Users\\...`` whose ``\\U`` reads as an invalid unicode escape) would
|
||||
# produce unparseable TOML — route those to the *literal* form ('''...'''),
|
||||
# which does not process escapes, or to the escaped basic string.
|
||||
if '"""' not in body and "\\" not in body:
|
||||
# Keep TOML output valid even when body contains triple-quote delimiters.
|
||||
# Prefer multiline forms, then fall back to escaped basic string.
|
||||
if '"""' not in body:
|
||||
toml_lines.append('prompt = """')
|
||||
toml_lines.append(body)
|
||||
toml_lines.append('"""')
|
||||
@@ -433,6 +426,37 @@ class CommandRegistrar:
|
||||
|
||||
body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name)
|
||||
|
||||
# Resolve __CONTEXT_FILE__ from the agent-context extension config.
|
||||
# When disabled, ignore stale context_files but keep the singular
|
||||
# context_file value so generated commands still point at the agent
|
||||
# context file managed before the extension was disabled.
|
||||
from .integrations.base import IntegrationBase
|
||||
|
||||
# Local import: _load_agent_context_config lives in __init__.py which
|
||||
# imports agents.py, so a top-level import would be circular.
|
||||
from . import _load_agent_context_config
|
||||
|
||||
ac_cfg = _load_agent_context_config(project_root)
|
||||
extension_enabled = IntegrationBase._agent_context_extension_enabled(
|
||||
project_root
|
||||
)
|
||||
if extension_enabled:
|
||||
context_files = IntegrationBase._resolve_context_file_values(
|
||||
project_root,
|
||||
ac_cfg,
|
||||
legacy_context_file=init_opts.get("context_file"),
|
||||
)
|
||||
else:
|
||||
context_files = IntegrationBase._resolve_context_file_values(
|
||||
project_root,
|
||||
ac_cfg,
|
||||
legacy_context_file=init_opts.get("context_file"),
|
||||
include_context_files=False,
|
||||
validate=False,
|
||||
)
|
||||
context_file = IntegrationBase._format_context_file_values(context_files)
|
||||
body = body.replace("__CONTEXT_FILE__", context_file)
|
||||
|
||||
return CommandRegistrar.rewrite_project_relative_paths(body)
|
||||
|
||||
def _convert_argument_placeholder(
|
||||
@@ -690,7 +714,6 @@ class CommandRegistrar:
|
||||
output_name,
|
||||
agent_config["extension"],
|
||||
link_outputs,
|
||||
agent_config,
|
||||
)
|
||||
|
||||
if agent_name == "copilot":
|
||||
@@ -765,7 +788,6 @@ class CommandRegistrar:
|
||||
alias_output_name,
|
||||
agent_config["extension"],
|
||||
link_outputs,
|
||||
agent_config,
|
||||
)
|
||||
if agent_name == "copilot":
|
||||
self.write_copilot_prompt(project_root, alias)
|
||||
@@ -782,12 +804,9 @@ class CommandRegistrar:
|
||||
output_name: str,
|
||||
extension: str,
|
||||
link_outputs: bool,
|
||||
agent_config: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Write a rendered agent artifact, optionally as a dev-mode symlink."""
|
||||
if not link_outputs or (agent_config or {}).get("dev_no_symlink"):
|
||||
if dest_file.is_symlink():
|
||||
dest_file.unlink()
|
||||
if not link_outputs:
|
||||
dest_file.write_text(content, encoding="utf-8")
|
||||
return
|
||||
|
||||
@@ -908,16 +927,6 @@ class CommandRegistrar:
|
||||
self._active_skills_agent(project_root)
|
||||
if create_missing_active_skills_dir else None
|
||||
)
|
||||
active_skills_dir: Optional[Path] = None
|
||||
if active_skills_agent:
|
||||
active_skills_config = self.AGENT_CONFIGS.get(active_skills_agent)
|
||||
if (
|
||||
active_skills_config
|
||||
and active_skills_config.get("extension") == "/SKILL.md"
|
||||
):
|
||||
active_skills_dir = self._resolve_agent_dir(
|
||||
active_skills_agent, active_skills_config, project_root,
|
||||
)
|
||||
active_created_skills_dir: Optional[Path] = None
|
||||
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
||||
active_skills_output = (
|
||||
@@ -949,14 +958,6 @@ class CommandRegistrar:
|
||||
agent_dir = self._resolve_agent_dir(
|
||||
agent_name, agent_config, project_root,
|
||||
)
|
||||
shares_active_skills_dir = (
|
||||
active_skills_dir is not None
|
||||
and agent_name != active_skills_agent
|
||||
and agent_config.get("extension") == "/SKILL.md"
|
||||
and self._same_lexical_path(agent_dir, active_skills_dir)
|
||||
)
|
||||
if shares_active_skills_dir:
|
||||
continue
|
||||
|
||||
agent_dir_existed = agent_dir.is_dir()
|
||||
register_missing_active_skills_agent = (
|
||||
|
||||
@@ -118,20 +118,6 @@ def build_request(url: str, extra_headers: dict[str, str] | None = None) -> urll
|
||||
return urllib.request.Request(url, headers=headers)
|
||||
|
||||
|
||||
def github_provider_hosts() -> tuple[str, ...]:
|
||||
"""Return host patterns from every ``github`` provider entry in ``auth.json``.
|
||||
|
||||
Used to classify which hosts are GitHub Enterprise Server instances when
|
||||
resolving release-asset download URLs. Returns an empty tuple when no
|
||||
``auth.json`` exists or it contains no ``github`` entries.
|
||||
"""
|
||||
hosts: list[str] = []
|
||||
for entry in _load_config():
|
||||
if entry.provider == "github":
|
||||
hosts.extend(entry.hosts)
|
||||
return tuple(hosts)
|
||||
|
||||
|
||||
def open_url(
|
||||
url: str,
|
||||
timeout: int = 10,
|
||||
|
||||
@@ -180,18 +180,9 @@ def remove_source(project_root: Path, id_or_url: str) -> str:
|
||||
)
|
||||
|
||||
catalogs = _read(project_root)
|
||||
# Prefer an exact id/url match.
|
||||
remaining = [c for c in catalogs if c.get("id") != target and c.get("url") != target]
|
||||
if len(remaining) == len(catalogs):
|
||||
# No exact match. add_source canonicalizes a local path to an absolute
|
||||
# url before storing, so fall back to a canonicalized-url match -- this
|
||||
# lets `remove ./cat.json` undo `add ./cat.json` (stored absolute).
|
||||
# Only as a *fallback*: _canonicalize_url treats a bare id as a local
|
||||
# path (empty scheme), so applying it unconditionally could also delete a
|
||||
# different source whose url equals the id's canonicalized path.
|
||||
canonical = _canonicalize_url(target)
|
||||
if canonical != target:
|
||||
remaining = [c for c in catalogs if c.get("url") != canonical]
|
||||
remaining = [
|
||||
c for c in catalogs if c.get("id") != target and c.get("url") != target
|
||||
]
|
||||
if len(remaining) == len(catalogs):
|
||||
raise BundlerError(
|
||||
f"No project-scoped catalog source matching '{target}' was found."
|
||||
|
||||
@@ -78,10 +78,7 @@ class CatalogStackBase:
|
||||
f"Catalog URL must use HTTPS (got {parsed.scheme}://). "
|
||||
"HTTP is only allowed for localhost."
|
||||
)
|
||||
# Check hostname, not netloc: netloc is truthy for host-less URLs like
|
||||
# "https://:8080" or "https://user@", so the host guarantee this error
|
||||
# promises would not actually hold. hostname is None in those cases (#3209).
|
||||
if not parsed.hostname:
|
||||
if not parsed.netloc:
|
||||
raise cls._error("Catalog URL must be a valid URL with a host.")
|
||||
|
||||
def _load_catalog_config(self, config_path: Path) -> list[CatalogEntry] | None:
|
||||
|
||||
@@ -13,7 +13,7 @@ from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from ..._console import console, err_console
|
||||
from ..._console import console
|
||||
from ...bundler import BundlerError
|
||||
from ...bundler.lib.project import (
|
||||
active_integration,
|
||||
@@ -41,9 +41,7 @@ bundle_app.add_typer(bundle_catalog_app, name="catalog")
|
||||
|
||||
def _fail(message: str) -> None:
|
||||
"""Print an actionable error to stderr and exit non-zero."""
|
||||
# Use the stderr console so the error never lands on stdout, which under
|
||||
# ``--json`` carries the machine-readable payload and must stay parseable.
|
||||
err_console.print(f"[red]Error:[/red] {message}", style=None)
|
||||
console.print(f"[red]Error:[/red] {message}", style=None)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from .._agent_config import (
|
||||
SCRIPT_TYPE_CHOICES,
|
||||
)
|
||||
from .._assets import (
|
||||
_locate_bundled_extension,
|
||||
_locate_bundled_preset,
|
||||
_locate_bundled_workflow,
|
||||
get_speckit_version,
|
||||
@@ -170,6 +171,7 @@ def register(app: typer.Typer) -> None:
|
||||
from .. import (
|
||||
_install_shared_infra_or_exit,
|
||||
_print_cli_warning,
|
||||
_update_agent_context_config_file,
|
||||
ensure_executable_scripts,
|
||||
save_init_options,
|
||||
)
|
||||
@@ -374,6 +376,7 @@ def register(app: typer.Typer) -> None:
|
||||
("chmod", "Ensure scripts executable"),
|
||||
("constitution", "Constitution setup"),
|
||||
("workflow", "Install bundled workflow"),
|
||||
("agent-context", "Install agent-context extension"),
|
||||
("final", "Finalize"),
|
||||
]:
|
||||
tracker.add(key, label)
|
||||
@@ -504,6 +507,47 @@ def register(app: typer.Typer) -> None:
|
||||
init_opts["ai_skills"] = True
|
||||
save_init_options(project_path, init_opts)
|
||||
|
||||
# --- agent-context extension (bundled, auto-installed) ---
|
||||
# Installed after init-options.json is written so that skill
|
||||
# registration can read ai_skills + integration key.
|
||||
try:
|
||||
from ..extensions import ExtensionManager as _ExtMgr
|
||||
|
||||
bundled_ac = _locate_bundled_extension("agent-context")
|
||||
if bundled_ac:
|
||||
ac_mgr = _ExtMgr(project_path)
|
||||
if ac_mgr.registry.is_installed("agent-context"):
|
||||
tracker.complete("agent-context", "already installed")
|
||||
else:
|
||||
ac_mgr.install_from_directory(
|
||||
bundled_ac, get_speckit_version()
|
||||
)
|
||||
tracker.complete("agent-context", "extension installed")
|
||||
else:
|
||||
from ..extensions import REINSTALL_COMMAND as _ac_reinstall
|
||||
|
||||
tracker.error(
|
||||
"agent-context",
|
||||
f"bundled extension not found — installation may be "
|
||||
f"incomplete. Run: {_ac_reinstall}",
|
||||
)
|
||||
except Exception as ac_err:
|
||||
sanitized_ac = str(ac_err).replace("\n", " ").strip()
|
||||
tracker.error(
|
||||
"agent-context",
|
||||
f"extension install failed: {sanitized_ac[:120]}",
|
||||
)
|
||||
|
||||
# Write context_file to the agent-context extension config
|
||||
# AFTER the extension install (which copies the template config
|
||||
# with an empty context_file).
|
||||
if resolved_integration.context_file:
|
||||
_update_agent_context_config_file(
|
||||
project_path,
|
||||
resolved_integration.context_file,
|
||||
preserve_markers=True,
|
||||
)
|
||||
|
||||
ensure_executable_scripts(project_path, tracker=tracker)
|
||||
|
||||
if preset:
|
||||
|
||||
@@ -28,10 +28,9 @@ from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
||||
|
||||
from .._init_options import is_ai_skills_enabled
|
||||
from .._invocation_style import is_dollar_skills_agent, is_slash_skills_agent
|
||||
from .._utils import dump_frontmatter, relative_extension_path_violation, version_satisfies
|
||||
from .._utils import dump_frontmatter, relative_extension_path_violation
|
||||
from ..catalogs import CatalogEntry as BaseCatalogEntry
|
||||
from ..catalogs import CatalogStackBase
|
||||
from ..shared_infra import verify_archive_sha256
|
||||
|
||||
_FALLBACK_CORE_COMMAND_NAMES = frozenset(
|
||||
{
|
||||
@@ -998,7 +997,6 @@ class ExtensionManager:
|
||||
if not isinstance(selected_ai, str) or not selected_ai:
|
||||
return []
|
||||
registrar = CommandRegistrar()
|
||||
agent_config = registrar.AGENT_CONFIGS.get(selected_ai, {})
|
||||
integration = get_integration(selected_ai)
|
||||
|
||||
for cmd_info in manifest.commands:
|
||||
@@ -1032,16 +1030,15 @@ class ExtensionManager:
|
||||
skill_file = skill_subdir / "SKILL.md"
|
||||
cache_root = extension_dir / ".specify-dev" / "extension-skills"
|
||||
cache_file = cache_root / skill_name / "SKILL.md"
|
||||
use_dev_symlink = link_outputs and not agent_config.get("dev_no_symlink")
|
||||
CommandRegistrar._ensure_inside(cache_file, cache_root)
|
||||
if skill_file.exists() or skill_file.is_symlink():
|
||||
is_expected_dev_symlink = self._is_expected_dev_symlink(
|
||||
skill_file, cache_file
|
||||
)
|
||||
# Do not overwrite user-customized skills, but allow dev-mode
|
||||
# symlinks that point back to this extension's generated cache
|
||||
# to be refreshed on a subsequent dev install.
|
||||
if not is_expected_dev_symlink:
|
||||
if not (
|
||||
link_outputs
|
||||
and self._is_expected_dev_symlink(skill_file, cache_file)
|
||||
):
|
||||
continue
|
||||
|
||||
# Create skill directory; track whether we created it so we can clean
|
||||
@@ -1096,7 +1093,7 @@ class ExtensionManager:
|
||||
):
|
||||
skill_content = integration.post_process_skill_content(skill_content)
|
||||
|
||||
if use_dev_symlink:
|
||||
if link_outputs:
|
||||
try:
|
||||
cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
cache_file.write_text(skill_content, encoding="utf-8")
|
||||
@@ -1109,8 +1106,6 @@ class ExtensionManager:
|
||||
skill_file.unlink()
|
||||
skill_file.write_text(skill_content, encoding="utf-8")
|
||||
else:
|
||||
if skill_file.is_symlink():
|
||||
skill_file.unlink()
|
||||
skill_file.write_text(skill_content, encoding="utf-8")
|
||||
written.append(skill_name)
|
||||
|
||||
@@ -1279,20 +1274,20 @@ class ExtensionManager:
|
||||
CompatibilityError: If extension is incompatible
|
||||
"""
|
||||
required = manifest.requires_speckit_version
|
||||
current = pkg_version.Version(speckit_version)
|
||||
|
||||
# Parse version specifier (e.g., ">=0.1.0,<2.0.0")
|
||||
try:
|
||||
SpecifierSet(required) # Just to validate
|
||||
specifier = SpecifierSet(required)
|
||||
if current not in specifier:
|
||||
raise CompatibilityError(
|
||||
f"Extension requires spec-kit {required}, "
|
||||
f"but {speckit_version} is installed.\n"
|
||||
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
|
||||
)
|
||||
except InvalidSpecifier:
|
||||
raise CompatibilityError(f"Invalid version specifier: {required}")
|
||||
|
||||
if not version_satisfies(speckit_version, required):
|
||||
raise CompatibilityError(
|
||||
f"Extension requires spec-kit {required}, "
|
||||
f"but {speckit_version} is installed.\n"
|
||||
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def install_from_directory(
|
||||
@@ -1871,6 +1866,24 @@ class ExtensionManager:
|
||||
return None
|
||||
|
||||
|
||||
def version_satisfies(current: str, required: str) -> bool:
|
||||
"""Check if current version satisfies required version specifier.
|
||||
|
||||
Args:
|
||||
current: Current version (e.g., "0.1.5")
|
||||
required: Required version specifier (e.g., ">=0.1.0,<2.0.0")
|
||||
|
||||
Returns:
|
||||
True if version satisfies requirement
|
||||
"""
|
||||
try:
|
||||
current_ver = pkg_version.Version(current)
|
||||
specifier = SpecifierSet(required)
|
||||
return current_ver in specifier
|
||||
except (pkg_version.InvalidVersion, InvalidSpecifier):
|
||||
return False
|
||||
|
||||
|
||||
class CommandRegistrar:
|
||||
"""Handles registration of extension commands with AI agents.
|
||||
|
||||
@@ -2039,18 +2052,12 @@ class ExtensionCatalog(CatalogStackBase):
|
||||
) -> Optional[str]:
|
||||
"""Resolve a GitHub release asset URL to its API asset URL.
|
||||
|
||||
Delegates to the shared helper in :mod:`specify_cli._github_http`,
|
||||
passing the ``github`` provider hosts from ``auth.json`` so GitHub
|
||||
Enterprise Server release assets resolve via ``/api/v3``.
|
||||
Delegates to the shared helper in :mod:`specify_cli._github_http`.
|
||||
"""
|
||||
from specify_cli._github_http import resolve_github_release_asset_api_url
|
||||
from specify_cli.authentication.http import github_provider_hosts
|
||||
|
||||
return resolve_github_release_asset_api_url(
|
||||
download_url,
|
||||
self._open_url,
|
||||
timeout=timeout,
|
||||
github_hosts=github_provider_hosts(),
|
||||
download_url, self._open_url, timeout=timeout
|
||||
)
|
||||
|
||||
def _validate_catalog_payload(self, catalog_data: Any, url: str) -> None:
|
||||
@@ -2610,10 +2617,6 @@ class ExtensionCatalog(CatalogStackBase):
|
||||
) as response:
|
||||
zip_data = response.read()
|
||||
|
||||
verify_archive_sha256(
|
||||
zip_data, ext_info.get("sha256"), extension_id, ExtensionError
|
||||
)
|
||||
|
||||
zip_path.write_bytes(zip_data)
|
||||
return zip_path
|
||||
|
||||
|
||||
@@ -482,7 +482,6 @@ def extension_add(
|
||||
|
||||
elif from_url:
|
||||
# Install from URL (ZIP file)
|
||||
import io
|
||||
import urllib.error
|
||||
|
||||
console.print(f"Downloading from {safe_url}...")
|
||||
@@ -499,33 +498,10 @@ def extension_add(
|
||||
zip_path = Path(download_file.name)
|
||||
|
||||
try:
|
||||
# Use the catalog's authenticated fetch so configured
|
||||
# credentials (incl. GitHub Enterprise Server) are applied
|
||||
# and GHES release-asset URLs resolve via /api/v3 — keeping
|
||||
# --from consistent with catalog-based installs.
|
||||
dl_catalog = ExtensionCatalog(project_root)
|
||||
download_url = from_url
|
||||
extra_headers = None
|
||||
resolved_url = dl_catalog._resolve_github_release_asset_api_url(download_url)
|
||||
if resolved_url:
|
||||
download_url = resolved_url
|
||||
extra_headers = {"Accept": "application/octet-stream"}
|
||||
from specify_cli.authentication.http import open_url as _open_url
|
||||
|
||||
with dl_catalog._open_url(
|
||||
download_url, timeout=60, extra_headers=extra_headers
|
||||
) as response:
|
||||
with _open_url(from_url, timeout=60) as response:
|
||||
zip_data = response.read()
|
||||
|
||||
if not zipfile.is_zipfile(io.BytesIO(zip_data)):
|
||||
console.print(
|
||||
f"[red]Error:[/red] {safe_url} did not return a ZIP archive "
|
||||
f"(got {len(zip_data)} bytes). This usually means the request "
|
||||
f"was not authenticated and a login/HTML page was returned. "
|
||||
f"Verify the URL is correct and that credentials for its host "
|
||||
f"are configured in ~/.specify/auth.json."
|
||||
)
|
||||
raise typer.Exit(1)
|
||||
|
||||
zip_path.write_bytes(zip_data)
|
||||
|
||||
# Install from downloaded ZIP
|
||||
|
||||
@@ -117,6 +117,11 @@ class {class_name}({template.base_class}):
|
||||
"args": "{template.args}",
|
||||
"extension": "{template.extension}",
|
||||
}}
|
||||
context_file = "AGENTS.md"
|
||||
# Default to False so the generated boilerplate passes the registry
|
||||
# contract out of the box: multi-install-safe integrations must each have a
|
||||
# distinct context_file, and the placeholder above ("AGENTS.md") collides
|
||||
# with the existing codex integration. Opt in once you pick a unique one.
|
||||
multi_install_safe = False
|
||||
'''
|
||||
|
||||
@@ -150,6 +155,7 @@ def test_metadata():
|
||||
assert integration.registrar_config["format"] == "{template.registrar_format}"
|
||||
assert integration.registrar_config["args"] == "{template.args}"
|
||||
assert integration.registrar_config["extension"] == "{template.extension}"
|
||||
assert integration.context_file == "AGENTS.md"
|
||||
assert integration.multi_install_safe is False
|
||||
'''
|
||||
|
||||
@@ -268,7 +274,7 @@ def scaffold_integration(
|
||||
|
||||
next_steps = (
|
||||
f"Register {class_name} in src/specify_cli/integrations/__init__.py.",
|
||||
"Review config metadata, install_url, requires_cli, and multi_install_safe.",
|
||||
"Review config metadata, install_url, requires_cli, context_file, and multi_install_safe.",
|
||||
f"Run pytest tests/integrations/test_integration_{package_name}.py -v.",
|
||||
)
|
||||
return IntegrationScaffoldResult(
|
||||
|
||||
@@ -64,21 +64,23 @@ def _register_builtins() -> None:
|
||||
from .generic import GenericIntegration
|
||||
from .goose import GooseIntegration
|
||||
from .hermes import HermesIntegration
|
||||
from .iflow import IflowIntegration
|
||||
from .junie import JunieIntegration
|
||||
from .kilocode import KilocodeIntegration
|
||||
from .kimi import KimiIntegration
|
||||
from .kiro_cli import KiroCliIntegration
|
||||
from .lingma import LingmaIntegration
|
||||
from .omp import OmpIntegration
|
||||
from .opencode import OpencodeIntegration
|
||||
from .pi import PiIntegration
|
||||
from .qodercli import QodercliIntegration
|
||||
from .qwen import QwenIntegration
|
||||
from .roo import RooIntegration
|
||||
from .rovodev import RovodevIntegration
|
||||
from .shai import ShaiIntegration
|
||||
from .tabnine import TabnineIntegration
|
||||
from .trae import TraeIntegration
|
||||
from .vibe import VibeIntegration
|
||||
from .windsurf import WindsurfIntegration
|
||||
from .zcode import ZcodeIntegration
|
||||
from .zed import ZedIntegration
|
||||
|
||||
@@ -100,21 +102,23 @@ def _register_builtins() -> None:
|
||||
_register(GenericIntegration())
|
||||
_register(GooseIntegration())
|
||||
_register(HermesIntegration())
|
||||
_register(IflowIntegration())
|
||||
_register(JunieIntegration())
|
||||
_register(KilocodeIntegration())
|
||||
_register(KimiIntegration())
|
||||
_register(KiroCliIntegration())
|
||||
_register(LingmaIntegration())
|
||||
_register(OmpIntegration())
|
||||
_register(OpencodeIntegration())
|
||||
_register(PiIntegration())
|
||||
_register(QodercliIntegration())
|
||||
_register(QwenIntegration())
|
||||
_register(RooIntegration())
|
||||
_register(RovodevIntegration())
|
||||
_register(ShaiIntegration())
|
||||
_register(TabnineIntegration())
|
||||
_register(TraeIntegration())
|
||||
_register(VibeIntegration())
|
||||
_register(WindsurfIntegration())
|
||||
_register(ZcodeIntegration())
|
||||
_register(ZedIntegration())
|
||||
|
||||
|
||||
@@ -103,17 +103,38 @@ def _refresh_init_options_speckit_version(project_root: Path) -> None:
|
||||
|
||||
|
||||
def _clear_init_options_for_integration(project_root: Path, integration_key: str) -> None:
|
||||
"""Clear active integration keys from init-options.json when they match."""
|
||||
"""Clear active integration keys from init-options.json when they match.
|
||||
|
||||
Also clears ``context_file`` from the agent-context extension config so
|
||||
no stale path is left behind when the integration is uninstalled.
|
||||
"""
|
||||
from .. import (
|
||||
_AGENT_CTX_EXT_CONFIG,
|
||||
_update_agent_context_config_file,
|
||||
load_init_options,
|
||||
save_init_options,
|
||||
)
|
||||
opts = load_init_options(project_root)
|
||||
has_legacy_context_keys = ("context_file" in opts) or ("context_markers" in opts)
|
||||
# Remove legacy fields that older versions may have written.
|
||||
opts.pop("context_file", None)
|
||||
opts.pop("context_markers", None)
|
||||
|
||||
if opts.get("integration") == integration_key or opts.get("ai") == integration_key:
|
||||
opts.pop("integration", None)
|
||||
opts.pop("ai", None)
|
||||
opts.pop("ai_skills", None)
|
||||
save_init_options(project_root, opts)
|
||||
# Clear context_file in the extension config if it already exists.
|
||||
# Avoid creating the config (and parent dirs) in projects where the
|
||||
# agent-context extension was never installed.
|
||||
ext_cfg_path = project_root / _AGENT_CTX_EXT_CONFIG
|
||||
if ext_cfg_path.exists():
|
||||
_update_agent_context_config_file(
|
||||
project_root, "", preserve_markers=True, preserve_context_files=False
|
||||
)
|
||||
elif has_legacy_context_keys:
|
||||
save_init_options(project_root, opts)
|
||||
|
||||
|
||||
def _remove_integration_json(project_root: Path) -> None:
|
||||
@@ -253,13 +274,21 @@ def _update_init_options_for_integration(
|
||||
integration: Any,
|
||||
script_type: str | None = None,
|
||||
) -> None:
|
||||
"""Update init-options.json to reflect *integration* as the active one.
|
||||
"""Update init-options.json and the agent-context extension config to
|
||||
reflect *integration* as the active one.
|
||||
|
||||
Agent context/instruction files are owned entirely by the opt-in
|
||||
agent-context extension, so this function never touches the extension
|
||||
or its config.
|
||||
``context_file``, ``context_files``, and ``context_markers`` are stored in the agent-context
|
||||
extension config (``.specify/extensions/agent-context/agent-context-config.yml``),
|
||||
not in ``init-options.json``. Existing user-customised markers are
|
||||
always preserved when the config already exists. Existing ``context_files``
|
||||
lists are also preserved so projects can keep multi-agent context anchors
|
||||
during integration switches. Invalid marker values are
|
||||
silently ignored at runtime by ``_resolve_context_markers()`` which falls
|
||||
back to the class-level defaults.
|
||||
"""
|
||||
from .. import (
|
||||
_AGENT_CTX_EXT_CONFIG,
|
||||
_update_agent_context_config_file,
|
||||
load_init_options,
|
||||
save_init_options,
|
||||
)
|
||||
@@ -267,6 +296,9 @@ def _update_init_options_for_integration(
|
||||
opts = load_init_options(project_root)
|
||||
opts["integration"] = integration.key
|
||||
opts["ai"] = integration.key
|
||||
# Remove legacy fields if they were written by an older version.
|
||||
opts.pop("context_file", None)
|
||||
opts.pop("context_markers", None)
|
||||
opts["speckit_version"] = _get_speckit_version()
|
||||
if script_type:
|
||||
opts["script"] = script_type
|
||||
@@ -275,6 +307,24 @@ def _update_init_options_for_integration(
|
||||
else:
|
||||
opts.pop("ai_skills", None)
|
||||
|
||||
# Update the agent-context extension config BEFORE init-options.json
|
||||
# so a failure here doesn't leave init-options partially updated.
|
||||
ext_cfg_path = project_root / _AGENT_CTX_EXT_CONFIG
|
||||
if ext_cfg_path.exists():
|
||||
_update_agent_context_config_file(
|
||||
project_root,
|
||||
integration.context_file,
|
||||
preserve_markers=True,
|
||||
)
|
||||
elif integration.context_file:
|
||||
# Extension config doesn't exist yet (extension not installed).
|
||||
# Write defaults so scripts have something to read.
|
||||
_update_agent_context_config_file(
|
||||
project_root,
|
||||
integration.context_file,
|
||||
preserve_markers=False,
|
||||
)
|
||||
|
||||
save_init_options(project_root, opts)
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class AgyIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@staticmethod
|
||||
def _inject_hook_command_note(content: str) -> str:
|
||||
|
||||
@@ -18,3 +18,4 @@ class AmpIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@@ -18,4 +18,5 @@ class AuggieIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = ".augment/rules/specify-rules.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@@ -13,13 +13,14 @@ Provides:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PureWindowsPath
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import yaml
|
||||
@@ -90,9 +91,13 @@ class IntegrationBase(ABC):
|
||||
|
||||
And may optionally set:
|
||||
|
||||
* ``invoke_separator`` — slash-command separator (defaults to ``"."``)
|
||||
* ``multi_install_safe`` — declare the integration safe to install
|
||||
alongside others (defaults to ``False``)
|
||||
* ``context_file`` — path (relative to project root) of the agent
|
||||
context/instructions file (e.g. ``"CLAUDE.md"``)
|
||||
|
||||
Projects may additionally opt into managing multiple context files by
|
||||
setting ``context_files`` in the agent-context extension config. The
|
||||
integration class still declares one default ``context_file`` for backwards
|
||||
compatibility and command-template rendering.
|
||||
"""
|
||||
|
||||
# -- Must be set by every subclass ------------------------------------
|
||||
@@ -108,20 +113,25 @@ class IntegrationBase(ABC):
|
||||
|
||||
# -- Optional ---------------------------------------------------------
|
||||
|
||||
context_file: str | None = None
|
||||
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
|
||||
|
||||
invoke_separator: str = "."
|
||||
"""Separator used in slash-command invocations (``"."`` → ``/speckit.plan``)."""
|
||||
|
||||
dev_no_symlink: bool = False
|
||||
"""Whether dev-mode registration should write files instead of symlinks."""
|
||||
|
||||
multi_install_safe: bool = False
|
||||
"""Whether this integration is declared safe to install alongside others.
|
||||
|
||||
Safe integrations must use a static, unique agent root and command
|
||||
directory. Registry tests enforce those invariants for every
|
||||
Safe integrations must use a static, unique agent root, command directory,
|
||||
and context file. Registry tests enforce those invariants for every
|
||||
integration that sets this flag.
|
||||
"""
|
||||
|
||||
# -- Markers for managed context section ------------------------------
|
||||
|
||||
CONTEXT_MARKER_START = "<!-- SPECKIT START -->"
|
||||
CONTEXT_MARKER_END = "<!-- SPECKIT END -->"
|
||||
|
||||
# -- Public API -------------------------------------------------------
|
||||
|
||||
@classmethod
|
||||
@@ -520,6 +530,498 @@ class IntegrationBase(ABC):
|
||||
|
||||
return created
|
||||
|
||||
# -- Agent context file management ------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _ensure_mdc_frontmatter(content: str) -> str:
|
||||
"""Ensure ``.mdc`` content has YAML frontmatter with ``alwaysApply: true``.
|
||||
|
||||
If frontmatter is missing, prepend it. If frontmatter exists but
|
||||
``alwaysApply`` is absent or not ``true``, inject/fix it.
|
||||
|
||||
Uses string/regex manipulation to preserve comments and formatting
|
||||
in existing frontmatter.
|
||||
"""
|
||||
import re as _re
|
||||
|
||||
leading_ws = len(content) - len(content.lstrip())
|
||||
leading = content[:leading_ws]
|
||||
stripped = content[leading_ws:]
|
||||
|
||||
if not stripped.startswith("---"):
|
||||
return "---\nalwaysApply: true\n---\n\n" + content
|
||||
|
||||
# Match frontmatter block: ---\n...\n---
|
||||
match = _re.match(
|
||||
r"^(---[ \t]*\r?\n)(.*?)(\r?\n---[ \t]*)(\r?\n|$)(.*)",
|
||||
stripped,
|
||||
_re.DOTALL,
|
||||
)
|
||||
if not match:
|
||||
return "---\nalwaysApply: true\n---\n\n" + content
|
||||
|
||||
opening, fm_text, closing, sep, rest = match.groups()
|
||||
newline = "\r\n" if "\r\n" in opening else "\n"
|
||||
|
||||
# Already correct?
|
||||
if _re.search(
|
||||
r"(?m)^[ \t]*alwaysApply[ \t]*:[ \t]*true[ \t]*(?:#.*)?$", fm_text
|
||||
):
|
||||
return content
|
||||
|
||||
# alwaysApply exists but wrong value — fix in place while preserving
|
||||
# indentation and any trailing inline comment.
|
||||
if _re.search(r"(?m)^[ \t]*alwaysApply[ \t]*:", fm_text):
|
||||
fm_text = _re.sub(
|
||||
r"(?m)^([ \t]*)alwaysApply[ \t]*:.*?([ \t]*(?:#.*)?)$",
|
||||
r"\1alwaysApply: true\2",
|
||||
fm_text,
|
||||
count=1,
|
||||
)
|
||||
elif fm_text.strip():
|
||||
fm_text = fm_text + newline + "alwaysApply: true"
|
||||
else:
|
||||
fm_text = "alwaysApply: true"
|
||||
|
||||
return f"{leading}{opening}{fm_text}{closing}{sep}{rest}"
|
||||
|
||||
@staticmethod
|
||||
def _build_context_section(plan_path: str = "") -> str:
|
||||
"""Build the content for the managed section between markers.
|
||||
|
||||
*plan_path* is the project-relative path to the current plan
|
||||
(e.g. ``"specs/<feature>/plan.md"``). When empty, the section
|
||||
contains only the generic directive without a concrete path.
|
||||
"""
|
||||
lines = [
|
||||
"For additional context about technologies to be used, project structure,",
|
||||
"shell commands, and other important information, read the current plan",
|
||||
]
|
||||
if plan_path:
|
||||
lines.append(f"at {plan_path}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _agent_context_extension_enabled(project_root: Path) -> bool:
|
||||
"""Return whether the bundled ``agent-context`` extension is enabled.
|
||||
|
||||
The extension is the single source of truth for managing coding
|
||||
agent context/instruction files (e.g. ``CLAUDE.md``,
|
||||
``.github/copilot-instructions.md``).
|
||||
|
||||
Returns ``True`` (enabled) when:
|
||||
- the extension registry does not exist (legacy project, backwards
|
||||
compatibility), or
|
||||
- the registry has no ``agent-context`` entry (older project layout
|
||||
predating the extension), or
|
||||
- the entry is present and not explicitly disabled.
|
||||
|
||||
Returns ``False`` only when an entry exists with ``enabled: false``.
|
||||
"""
|
||||
registry_path = (
|
||||
project_root / ".specify" / "extensions" / ".registry"
|
||||
)
|
||||
if not registry_path.exists():
|
||||
return True
|
||||
try:
|
||||
data = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError, UnicodeError):
|
||||
return True
|
||||
if not isinstance(data, dict):
|
||||
return True
|
||||
extensions = data.get("extensions")
|
||||
if not isinstance(extensions, dict):
|
||||
return True
|
||||
entry = extensions.get("agent-context")
|
||||
if not isinstance(entry, dict):
|
||||
return True
|
||||
return entry.get("enabled", True) is not False
|
||||
|
||||
@staticmethod
|
||||
def _context_file_dedupe_key(path: str) -> str:
|
||||
"""Return the comparison key for context file de-duplication."""
|
||||
return path.casefold() if os.name == "nt" else path
|
||||
|
||||
def _resolve_context_markers(self, project_root: Path) -> tuple[str, str]:
|
||||
"""Return the (start, end) context markers to use for *project_root*.
|
||||
|
||||
Reads ``context_markers.start`` / ``context_markers.end`` from the
|
||||
agent-context extension config
|
||||
(``.specify/extensions/agent-context/agent-context-config.yml``)
|
||||
when present. Falls back to the class-level constants
|
||||
``CONTEXT_MARKER_START`` / ``CONTEXT_MARKER_END`` when the file is
|
||||
missing, the section is absent, or the values are not non-empty
|
||||
strings.
|
||||
"""
|
||||
from .._console import console # local import to avoid cycles
|
||||
|
||||
start = self.CONTEXT_MARKER_START
|
||||
end = self.CONTEXT_MARKER_END
|
||||
config_path = (
|
||||
project_root
|
||||
/ ".specify"
|
||||
/ "extensions"
|
||||
/ "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
try:
|
||||
raw = config_path.read_text(encoding="utf-8")
|
||||
cfg = yaml.safe_load(raw)
|
||||
except (OSError, UnicodeError, ValueError, yaml.YAMLError):
|
||||
return start, end
|
||||
markers = cfg.get("context_markers") if isinstance(cfg, dict) else None
|
||||
if isinstance(markers, dict):
|
||||
cm_start = markers.get("start")
|
||||
cm_end = markers.get("end")
|
||||
s_valid = isinstance(cm_start, str) and cm_start
|
||||
e_valid = isinstance(cm_end, str) and cm_end
|
||||
if not s_valid and cm_start is not None:
|
||||
console.print(
|
||||
f"[yellow]agent-context: ignoring invalid context_markers.start "
|
||||
f"({cm_start!r}), using default[/yellow]"
|
||||
)
|
||||
if not e_valid and cm_end is not None:
|
||||
console.print(
|
||||
f"[yellow]agent-context: ignoring invalid context_markers.end "
|
||||
f"({cm_end!r}), using default[/yellow]"
|
||||
)
|
||||
if s_valid:
|
||||
start = cm_start # type: ignore[assignment]
|
||||
if e_valid:
|
||||
end = cm_end # type: ignore[assignment]
|
||||
return start, end
|
||||
|
||||
@staticmethod
|
||||
def _validate_context_file_path(project_root: Path, context_file: str) -> str:
|
||||
"""Return a safe project-relative context file path.
|
||||
|
||||
The agent-context scripts reject paths that can escape the project
|
||||
root; the Python integration path must apply the same guard before
|
||||
setup or teardown touches context files.
|
||||
"""
|
||||
candidate = context_file.strip()
|
||||
if not candidate:
|
||||
raise ValueError("agent-context: context file path must not be empty")
|
||||
|
||||
win_path = PureWindowsPath(candidate)
|
||||
if Path(candidate).is_absolute() or win_path.drive or win_path.root:
|
||||
raise ValueError(
|
||||
"agent-context: context files must be project-relative paths; "
|
||||
f"got {candidate!r}"
|
||||
)
|
||||
if "\\" in candidate:
|
||||
raise ValueError(
|
||||
"agent-context: context files must not contain backslash "
|
||||
f"separators; got {candidate!r}"
|
||||
)
|
||||
|
||||
parts = [part for part in re.split(r"[\\/]+", candidate) if part]
|
||||
if ".." in parts:
|
||||
raise ValueError(
|
||||
"agent-context: context files must not contain '..' path "
|
||||
f"segments; got {candidate!r}"
|
||||
)
|
||||
|
||||
root = project_root.resolve()
|
||||
target = (root / candidate).resolve(strict=False)
|
||||
try:
|
||||
target.relative_to(root)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
"agent-context: context file path resolves outside the project "
|
||||
f"root; got {candidate!r}"
|
||||
) from exc
|
||||
|
||||
return candidate
|
||||
|
||||
@classmethod
|
||||
def _resolve_context_file_values(
|
||||
cls,
|
||||
project_root: Path,
|
||||
cfg: dict[str, Any] | None,
|
||||
*,
|
||||
fallback_context_file: Any = None,
|
||||
legacy_context_file: Any = None,
|
||||
include_context_files: bool = True,
|
||||
validate: bool = True,
|
||||
) -> list[str]:
|
||||
"""Resolve context file config with shared precedence and de-duplication."""
|
||||
files: list[str] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def add_context_file(value: Any) -> None:
|
||||
if not isinstance(value, str):
|
||||
return
|
||||
candidate = value.strip()
|
||||
if not candidate:
|
||||
return
|
||||
if validate:
|
||||
candidate = cls._validate_context_file_path(project_root, candidate)
|
||||
key = cls._context_file_dedupe_key(candidate)
|
||||
if key in seen:
|
||||
return
|
||||
files.append(candidate)
|
||||
seen.add(key)
|
||||
|
||||
if isinstance(cfg, dict) and include_context_files:
|
||||
configured = cfg.get("context_files")
|
||||
if isinstance(configured, list):
|
||||
for value in configured:
|
||||
add_context_file(value)
|
||||
if files:
|
||||
return files
|
||||
|
||||
if isinstance(cfg, dict):
|
||||
add_context_file(cfg.get("context_file"))
|
||||
if files:
|
||||
return files
|
||||
|
||||
add_context_file(fallback_context_file)
|
||||
if files:
|
||||
return files
|
||||
|
||||
add_context_file(legacy_context_file)
|
||||
return files
|
||||
|
||||
@staticmethod
|
||||
def _format_context_file_values(context_files: list[str]) -> str:
|
||||
"""Return context file targets as the template display string."""
|
||||
return ", ".join(context_files)
|
||||
|
||||
def _resolve_context_files(self, project_root: Path) -> list[str]:
|
||||
"""Return project-relative context files managed for *project_root*.
|
||||
|
||||
``context_files`` in the agent-context extension config, when present
|
||||
and non-empty, takes precedence over the config's singular
|
||||
``context_file``. The integration class default is used only when the
|
||||
extension config has no context file target.
|
||||
Raises ``ValueError`` when a configured path can escape the project
|
||||
root.
|
||||
"""
|
||||
config_path = (
|
||||
project_root
|
||||
/ ".specify"
|
||||
/ "extensions"
|
||||
/ "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
try:
|
||||
raw = config_path.read_text(encoding="utf-8")
|
||||
cfg = yaml.safe_load(raw)
|
||||
except (OSError, UnicodeError, ValueError, yaml.YAMLError):
|
||||
cfg = None
|
||||
return self._resolve_context_file_values(
|
||||
project_root,
|
||||
cfg,
|
||||
fallback_context_file=self.context_file,
|
||||
)
|
||||
|
||||
def _context_file_display(self, project_root: Path) -> str:
|
||||
"""Return human-readable context file target(s) for templates."""
|
||||
if not self._agent_context_extension_enabled(project_root):
|
||||
from .. import _load_agent_context_config
|
||||
|
||||
context_files = self._resolve_context_file_values(
|
||||
project_root,
|
||||
_load_agent_context_config(project_root),
|
||||
fallback_context_file=self.context_file,
|
||||
include_context_files=False,
|
||||
validate=False,
|
||||
)
|
||||
return context_files[0] if context_files else ""
|
||||
return self._format_context_file_values(
|
||||
self._resolve_context_files(project_root)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _upsert_context_file(
|
||||
ctx_path: Path,
|
||||
section: str,
|
||||
marker_start: str,
|
||||
marker_end: str,
|
||||
) -> None:
|
||||
"""Create or update one managed context section."""
|
||||
if ctx_path.exists():
|
||||
content = ctx_path.read_text(encoding="utf-8-sig")
|
||||
start_idx = content.find(marker_start)
|
||||
end_idx = content.find(
|
||||
marker_end,
|
||||
start_idx if start_idx != -1 else 0,
|
||||
)
|
||||
|
||||
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
||||
# Replace existing section (include the end marker + newline)
|
||||
end_of_marker = end_idx + len(marker_end)
|
||||
# Consume trailing line ending (CRLF or LF)
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
||||
end_of_marker += 1
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
||||
end_of_marker += 1
|
||||
new_content = content[:start_idx] + section + content[end_of_marker:]
|
||||
elif start_idx != -1:
|
||||
# Corrupted: start marker without end — replace from start through EOF
|
||||
new_content = content[:start_idx] + section
|
||||
elif end_idx != -1:
|
||||
# Corrupted: end marker without start — replace BOF through end marker
|
||||
end_of_marker = end_idx + len(marker_end)
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
||||
end_of_marker += 1
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
||||
end_of_marker += 1
|
||||
new_content = section + content[end_of_marker:]
|
||||
else:
|
||||
# No markers found — append
|
||||
if content:
|
||||
if not content.endswith("\n"):
|
||||
content += "\n"
|
||||
new_content = content + "\n" + section
|
||||
else:
|
||||
new_content = section
|
||||
|
||||
# Ensure .mdc files have required YAML frontmatter
|
||||
if ctx_path.suffix == ".mdc":
|
||||
new_content = IntegrationBase._ensure_mdc_frontmatter(new_content)
|
||||
else:
|
||||
ctx_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Cursor .mdc files require YAML frontmatter to be loaded
|
||||
if ctx_path.suffix == ".mdc":
|
||||
new_content = IntegrationBase._ensure_mdc_frontmatter(section)
|
||||
else:
|
||||
new_content = section
|
||||
|
||||
normalized = new_content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
ctx_path.write_bytes(normalized.encode("utf-8"))
|
||||
|
||||
def upsert_context_section(
|
||||
self,
|
||||
project_root: Path,
|
||||
plan_path: str = "",
|
||||
) -> Path | None:
|
||||
"""Create or update the managed section in the agent context file.
|
||||
|
||||
If the context file does not exist it is created with just the
|
||||
managed section. If it exists, the content between the configured
|
||||
start/end markers (default ``<!-- SPECKIT START -->`` /
|
||||
``<!-- SPECKIT END -->``) is replaced, or appended when no markers
|
||||
are found. Markers are read from the agent-context extension config
|
||||
(``.specify/extensions/agent-context/agent-context-config.yml``)
|
||||
when present, falling back to the class-level constants.
|
||||
|
||||
Returns the path to the first context file, or ``None`` when no context
|
||||
files are configured or the ``agent-context`` extension is
|
||||
disabled.
|
||||
"""
|
||||
if not self._agent_context_extension_enabled(project_root):
|
||||
return None
|
||||
|
||||
context_files = self._resolve_context_files(project_root)
|
||||
if not context_files:
|
||||
return None
|
||||
|
||||
from .._console import console # local import to avoid cycles
|
||||
|
||||
console.print(
|
||||
"[yellow]Deprecation:[/yellow] Inline agent-context updates during "
|
||||
"integration setup will be disabled in v0.12.0. Context file "
|
||||
"management has moved to the bundled [bold]agent-context[/bold] "
|
||||
"extension. Run [cyan]specify extension disable agent-context[/cyan] "
|
||||
"to opt out early.",
|
||||
highlight=False,
|
||||
)
|
||||
|
||||
marker_start, marker_end = self._resolve_context_markers(project_root)
|
||||
|
||||
section = (
|
||||
f"{marker_start}\n"
|
||||
f"{self._build_context_section(plan_path)}\n"
|
||||
f"{marker_end}\n"
|
||||
)
|
||||
|
||||
first_path: Path | None = None
|
||||
for context_file in context_files:
|
||||
ctx_path = project_root / context_file
|
||||
self._upsert_context_file(ctx_path, section, marker_start, marker_end)
|
||||
if first_path is None:
|
||||
first_path = ctx_path
|
||||
return first_path
|
||||
|
||||
def remove_context_section(self, project_root: Path) -> bool:
|
||||
"""Remove the managed section from the agent context file.
|
||||
|
||||
Returns ``True`` if the section was found and removed. If the
|
||||
file becomes empty (or whitespace-only) after removal it is deleted.
|
||||
Markers are read from the agent-context extension config
|
||||
(``.specify/extensions/agent-context/agent-context-config.yml``)
|
||||
when present, falling back to the class-level constants.
|
||||
"""
|
||||
if not self._agent_context_extension_enabled(project_root):
|
||||
return False
|
||||
|
||||
context_files = self._resolve_context_files(project_root)
|
||||
if not context_files:
|
||||
return False
|
||||
|
||||
marker_start, marker_end = self._resolve_context_markers(project_root)
|
||||
removed_any = False
|
||||
|
||||
for context_file in context_files:
|
||||
ctx_path = project_root / context_file
|
||||
if not ctx_path.exists():
|
||||
continue
|
||||
|
||||
content = ctx_path.read_text(encoding="utf-8-sig")
|
||||
start_idx = content.find(marker_start)
|
||||
end_idx = content.find(
|
||||
marker_end,
|
||||
start_idx if start_idx != -1 else 0,
|
||||
)
|
||||
|
||||
# Only remove a complete, well-ordered managed section. If either
|
||||
# marker is missing, leave the file unchanged to avoid deleting
|
||||
# unrelated user-authored content.
|
||||
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
|
||||
continue
|
||||
|
||||
removal_start = start_idx
|
||||
removal_end = end_idx + len(marker_end)
|
||||
|
||||
# Consume trailing line ending (CRLF or LF)
|
||||
if removal_end < len(content) and content[removal_end] == "\r":
|
||||
removal_end += 1
|
||||
if removal_end < len(content) and content[removal_end] == "\n":
|
||||
removal_end += 1
|
||||
|
||||
# Also strip a blank line before the section if present
|
||||
if removal_start > 0 and content[removal_start - 1] == "\n":
|
||||
if removal_start > 1 and content[removal_start - 2] == "\n":
|
||||
removal_start -= 1
|
||||
|
||||
new_content = content[:removal_start] + content[removal_end:]
|
||||
|
||||
# Normalize line endings before comparisons
|
||||
normalized = new_content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
# For .mdc files, treat Speckit-generated frontmatter-only content as empty
|
||||
if ctx_path.suffix == ".mdc":
|
||||
import re
|
||||
|
||||
# Delete the file if only YAML frontmatter remains (no body content)
|
||||
frontmatter_only = re.match(
|
||||
r"^---\n.*?\n---\s*$", normalized, re.DOTALL
|
||||
)
|
||||
if not normalized.strip() or frontmatter_only:
|
||||
ctx_path.unlink()
|
||||
removed_any = True
|
||||
continue
|
||||
|
||||
if not normalized.strip():
|
||||
ctx_path.unlink()
|
||||
else:
|
||||
ctx_path.write_bytes(normalized.encode("utf-8"))
|
||||
removed_any = True
|
||||
|
||||
return removed_any
|
||||
|
||||
@staticmethod
|
||||
def resolve_command_refs(content: str, separator: str = ".") -> str:
|
||||
"""Replace ``__SPECKIT_COMMAND_<NAME>__`` placeholders with invocations.
|
||||
@@ -544,6 +1046,7 @@ class IntegrationBase(ABC):
|
||||
agent_name: str,
|
||||
script_type: str,
|
||||
arg_placeholder: str = "$ARGUMENTS",
|
||||
context_file: str = "",
|
||||
invoke_separator: str = ".",
|
||||
) -> str:
|
||||
"""Process a raw command template into agent-ready content.
|
||||
@@ -554,8 +1057,9 @@ class IntegrationBase(ABC):
|
||||
3. Strip ``scripts:`` section from frontmatter
|
||||
4. Replace ``{ARGS}`` and ``$ARGUMENTS`` with *arg_placeholder*
|
||||
5. Replace ``__AGENT__`` with *agent_name*
|
||||
6. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
||||
7. Replace ``__SPECKIT_COMMAND_<NAME>__`` with invocation strings
|
||||
6. Replace ``__CONTEXT_FILE__`` with *context_file*
|
||||
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
|
||||
8. Replace ``__SPECKIT_COMMAND_<NAME>__`` with invocation strings
|
||||
"""
|
||||
# 1. Extract script command from frontmatter
|
||||
script_command = ""
|
||||
@@ -615,7 +1119,10 @@ class IntegrationBase(ABC):
|
||||
# 5. Replace __AGENT__
|
||||
content = content.replace("__AGENT__", agent_name)
|
||||
|
||||
# 6. Rewrite paths — delegate to the shared implementation in
|
||||
# 6. Replace __CONTEXT_FILE__
|
||||
content = content.replace("__CONTEXT_FILE__", context_file)
|
||||
|
||||
# 7. Rewrite paths — delegate to the shared implementation in
|
||||
# CommandRegistrar so extension-local paths are preserved and
|
||||
# boundary rules stay consistent across the codebase.
|
||||
from specify_cli.agents import CommandRegistrar
|
||||
@@ -670,6 +1177,8 @@ class IntegrationBase(ABC):
|
||||
self.record_file_in_manifest(dst_file, project_root, manifest)
|
||||
created.append(dst_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
@@ -684,9 +1193,11 @@ class IntegrationBase(ABC):
|
||||
|
||||
Delegates to ``manifest.uninstall()`` which only removes files
|
||||
whose hash still matches the recorded value (unless *force*).
|
||||
Also removes the managed context section from the agent file.
|
||||
|
||||
Returns ``(removed, skipped)`` file lists.
|
||||
"""
|
||||
self.remove_context_section(project_root)
|
||||
return manifest.uninstall(project_root, force=force)
|
||||
|
||||
# -- Convenience helpers for subclasses -------------------------------
|
||||
@@ -720,11 +1231,12 @@ class IntegrationBase(ABC):
|
||||
class MarkdownIntegration(IntegrationBase):
|
||||
"""Concrete base for integrations that use standard Markdown commands.
|
||||
|
||||
Subclasses only need to set ``key``, ``config``, ``registrar_config``.
|
||||
Everything else is inherited.
|
||||
Subclasses only need to set ``key``, ``config``, ``registrar_config``
|
||||
(and optionally ``context_file``). Everything else is inherited.
|
||||
|
||||
``setup()`` processes command templates (replacing ``{SCRIPT}``,
|
||||
``{ARGS}``, ``__AGENT__``, rewriting paths).
|
||||
``{ARGS}``, ``__AGENT__``, rewriting paths) and upserts the
|
||||
managed context section into the agent context file.
|
||||
"""
|
||||
|
||||
def build_exec_args(
|
||||
@@ -779,11 +1291,13 @@ class MarkdownIntegration(IntegrationBase):
|
||||
else "$ARGUMENTS"
|
||||
)
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
dst_file = self.write_file_and_record(
|
||||
@@ -791,6 +1305,8 @@ class MarkdownIntegration(IntegrationBase):
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
@@ -804,7 +1320,8 @@ class TomlIntegration(IntegrationBase):
|
||||
"""Concrete base for integrations that use TOML command format.
|
||||
|
||||
Mirrors ``MarkdownIntegration`` closely: subclasses only need to set
|
||||
``key``, ``config``, ``registrar_config``. Everything else is inherited.
|
||||
``key``, ``config``, ``registrar_config`` (and optionally
|
||||
``context_file``). Everything else is inherited.
|
||||
|
||||
``setup()`` processes command templates through the same placeholder
|
||||
pipeline as ``MarkdownIntegration``, then converts the result to
|
||||
@@ -980,12 +1497,14 @@ class TomlIntegration(IntegrationBase):
|
||||
else "{{args}}"
|
||||
)
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
description = self._extract_description(raw)
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
)
|
||||
_, body = self._split_frontmatter(processed)
|
||||
toml_content = self._render_toml(description, body)
|
||||
@@ -995,6 +1514,8 @@ class TomlIntegration(IntegrationBase):
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
@@ -1008,7 +1529,8 @@ class YamlIntegration(IntegrationBase):
|
||||
"""Concrete base for integrations that use YAML recipe format.
|
||||
|
||||
Mirrors ``TomlIntegration`` closely: subclasses only need to set
|
||||
``key``, ``config``, ``registrar_config``. Everything else is inherited.
|
||||
``key``, ``config``, ``registrar_config`` (and optionally
|
||||
``context_file``). Everything else is inherited.
|
||||
|
||||
``setup()`` processes command templates through the same placeholder
|
||||
pipeline as ``MarkdownIntegration``, then converts the result to
|
||||
@@ -1171,6 +1693,7 @@ class YamlIntegration(IntegrationBase):
|
||||
else "{{args}}"
|
||||
)
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
@@ -1186,6 +1709,7 @@ class YamlIntegration(IntegrationBase):
|
||||
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
)
|
||||
_, body = self._split_frontmatter(processed)
|
||||
yaml_content = self._render_yaml(
|
||||
@@ -1197,6 +1721,8 @@ class YamlIntegration(IntegrationBase):
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
@@ -1212,8 +1738,8 @@ class SkillsIntegration(IntegrationBase):
|
||||
Skills use the ``speckit-<name>/SKILL.md`` directory layout following
|
||||
the `agentskills.io <https://agentskills.io/specification>`_ spec.
|
||||
|
||||
Subclasses set ``key``, ``config``, ``registrar_config`` like any
|
||||
integration. They may also
|
||||
Subclasses set ``key``, ``config``, ``registrar_config`` (and
|
||||
optionally ``context_file``) like any integration. They may also
|
||||
override ``options()`` to declare additional CLI flags (e.g.
|
||||
``--skills``, ``--migrate-legacy``).
|
||||
|
||||
@@ -1358,6 +1884,7 @@ class SkillsIntegration(IntegrationBase):
|
||||
else "$ARGUMENTS"
|
||||
)
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
@@ -1381,6 +1908,7 @@ class SkillsIntegration(IntegrationBase):
|
||||
# Process body through the standard template pipeline
|
||||
processed_body = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
invoke_separator=self.invoke_separator,
|
||||
)
|
||||
# Strip the processed frontmatter — we rebuild it for skills.
|
||||
@@ -1427,5 +1955,7 @@ class SkillsIntegration(IntegrationBase):
|
||||
)
|
||||
created.append(dst)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
@@ -18,3 +18,4 @@ class BobIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@@ -22,17 +22,13 @@ ARGUMENT_HINTS: dict[str, str] = {
|
||||
}
|
||||
|
||||
# Per-command frontmatter overrides for skills that should run in a forked
|
||||
# subagent context. See https://code.claude.com/docs/en/skills#run-skills-in-a-subagent
|
||||
#
|
||||
# This is intentionally empty. ``analyze`` was previously forked (added in
|
||||
# #2511) on the assumption that its heavy reads collapse to a short summary,
|
||||
# but in practice ``/speckit-analyze`` returns a 300-500 line report that is
|
||||
# injected back into the main conversation. In long sessions each subsequent
|
||||
# fork inherits that growing context, compounding overhead until the chat
|
||||
# freezes (#3185). Until a command genuinely returns a compact result, no
|
||||
# command opts into ``context: fork``. The injection mechanism below stays in
|
||||
# place so a future command can be added here when that holds true.
|
||||
FORK_CONTEXT_COMMANDS: dict[str, dict[str, str]] = {}
|
||||
# subagent context. Read-only analysis commands are good candidates: the
|
||||
# heavy reads (spec/plan/tasks artefacts) collapse to a short summary,
|
||||
# so isolating them keeps the main conversation context clean.
|
||||
# See https://code.claude.com/docs/en/skills#run-skills-in-a-subagent
|
||||
FORK_CONTEXT_COMMANDS: dict[str, dict[str, str]] = {
|
||||
"analyze": {"context": "fork", "agent": "general-purpose"},
|
||||
}
|
||||
|
||||
|
||||
class ClaudeIntegration(SkillsIntegration):
|
||||
@@ -52,6 +48,7 @@ class ClaudeIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = "CLAUDE.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -70,6 +70,7 @@ class ClineIntegration(MarkdownIntegration):
|
||||
"format_name": format_cline_command_name,
|
||||
"invoke_separator": "-",
|
||||
}
|
||||
context_file = ".clinerules/specify-rules.md"
|
||||
invoke_separator = "-"
|
||||
multi_install_safe = True
|
||||
|
||||
@@ -96,11 +97,7 @@ class ClineIntegration(MarkdownIntegration):
|
||||
def repl(m: re.Match[str]) -> str:
|
||||
indent = m.group(1)
|
||||
instruction = m.group(2)
|
||||
# ``eol`` is empty when the regex matched via ``$`` because the
|
||||
# instruction was the final line of a file with no trailing
|
||||
# newline. Default to ``\n`` so the note never collapses onto
|
||||
# the same line as the instruction.
|
||||
eol = m.group(3) or "\n"
|
||||
eol = m.group(3)
|
||||
return (
|
||||
indent
|
||||
+ _HOOK_COMMAND_NOTE.rstrip("\n")
|
||||
|
||||
@@ -9,7 +9,7 @@ class CodebuddyIntegration(MarkdownIntegration):
|
||||
"name": "CodeBuddy",
|
||||
"folder": ".codebuddy/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": "https://www.codebuddy.cn/docs/cli/installation",
|
||||
"install_url": "https://www.codebuddy.ai/cli",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
@@ -18,4 +18,5 @@ class CodebuddyIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "CODEBUDDY.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@@ -26,7 +26,7 @@ class CodexIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
dev_no_symlink = True
|
||||
context_file = "AGENTS.md"
|
||||
multi_install_safe = True
|
||||
|
||||
def build_exec_args(
|
||||
|
||||
@@ -4,6 +4,7 @@ Copilot has several unique behaviors compared to standard markdown agents:
|
||||
- Commands use ``.agent.md`` extension (not ``.md``)
|
||||
- Each command gets a companion ``.prompt.md`` file in ``.github/prompts/``
|
||||
- Installs ``.vscode/settings.json`` with prompt file recommendations
|
||||
- Context file lives at ``.github/copilot-instructions.md``
|
||||
|
||||
When ``--skills`` is passed via ``--integration-options``, Copilot scaffolds
|
||||
commands as ``speckit-<name>/SKILL.md`` directories under ``.github/skills/``
|
||||
@@ -57,17 +58,6 @@ def _allow_all() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _warn_legacy_markdown_default() -> None:
|
||||
"""Warn that Copilot's default markdown scaffold is being phased out."""
|
||||
warnings.warn(
|
||||
"Copilot legacy markdown mode is deprecated and will stop being the "
|
||||
'default in a future Spec Kit release; pass --integration-options "--skills" '
|
||||
"to opt in to Copilot skills mode now.",
|
||||
UserWarning,
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
|
||||
class _CopilotSkillsHelper(SkillsIntegration):
|
||||
"""Internal helper used when Copilot is scaffolded in skills mode.
|
||||
|
||||
@@ -89,6 +79,7 @@ class _CopilotSkillsHelper(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = ".github/copilot-instructions.md"
|
||||
|
||||
|
||||
class CopilotIntegration(IntegrationBase):
|
||||
@@ -117,6 +108,7 @@ class CopilotIntegration(IntegrationBase):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".agent.md",
|
||||
}
|
||||
context_file = ".github/copilot-instructions.md"
|
||||
|
||||
# Mutable flag set by setup() — indicates the active scaffolding mode.
|
||||
_skills_mode: bool = False
|
||||
@@ -327,8 +319,6 @@ class CopilotIntegration(IntegrationBase):
|
||||
self._skills_mode = bool(parsed_options.get("skills"))
|
||||
if self._skills_mode:
|
||||
return self._setup_skills(project_root, manifest, parsed_options, **opts)
|
||||
if "skills" not in parsed_options:
|
||||
_warn_legacy_markdown_default()
|
||||
return self._setup_default(project_root, manifest, parsed_options, **opts)
|
||||
|
||||
def _setup_default(
|
||||
@@ -364,12 +354,14 @@ class CopilotIntegration(IntegrationBase):
|
||||
|
||||
script_type = opts.get("script_type", "sh")
|
||||
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS")
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
# 1. Process and write command files as .agent.md
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
dst_file = self.write_file_and_record(
|
||||
@@ -404,6 +396,8 @@ class CopilotIntegration(IntegrationBase):
|
||||
self.record_file_in_manifest(dst_settings, project_root, manifest)
|
||||
created.append(dst_settings)
|
||||
|
||||
# 4. Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class CursorAgentIntegration(SkillsIntegration):
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
|
||||
context_file = ".cursor/rules/specify-rules.mdc"
|
||||
multi_install_safe = True
|
||||
|
||||
def build_exec_args(
|
||||
|
||||
@@ -30,6 +30,7 @@ class DevinIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
def build_exec_args(
|
||||
self,
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
Firebender (https://firebender.com/) is an AI coding agent for Android Studio
|
||||
and IntelliJ. It reads project-local custom slash commands from
|
||||
``.firebender/commands/*.mdc`` and project rules from ``.firebender/rules/*.mdc``,
|
||||
so Spec Kit installs its command templates as ``.mdc`` command files. The managed
|
||||
context section (when used) is owned by the ``agent-context`` extension.
|
||||
so Spec Kit installs its command templates as ``.mdc`` command files and writes
|
||||
the managed context section into a ``.firebender/rules/`` rule file.
|
||||
"""
|
||||
|
||||
from ..base import MarkdownIntegration
|
||||
@@ -25,6 +25,7 @@ class FirebenderIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".mdc",
|
||||
}
|
||||
context_file = ".firebender/rules/specify-rules.mdc"
|
||||
multi_install_safe = True
|
||||
|
||||
def command_filename(self, template_name: str) -> str:
|
||||
|
||||
@@ -89,6 +89,7 @@ class ForgeIntegration(MarkdownIntegration):
|
||||
"format_name": format_forge_command_name, # Custom name formatter
|
||||
"invoke_separator": "-",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
invoke_separator = "-"
|
||||
|
||||
def setup(
|
||||
@@ -127,12 +128,14 @@ class ForgeIntegration(MarkdownIntegration):
|
||||
script_type = opts.get("script_type", "sh")
|
||||
arg_placeholder = self.registrar_config.get("args", "{{parameters}}")
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
# Process template with standard MarkdownIntegration logic
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
invoke_separator=self.invoke_separator,
|
||||
)
|
||||
|
||||
@@ -149,6 +152,8 @@ class ForgeIntegration(MarkdownIntegration):
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
|
||||
@@ -18,4 +18,5 @@ class GeminiIntegration(TomlIntegration):
|
||||
"args": "{{args}}",
|
||||
"extension": ".toml",
|
||||
}
|
||||
context_file = "GEMINI.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@@ -31,6 +31,7 @@ class GenericIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
@@ -118,11 +119,13 @@ class GenericIntegration(MarkdownIntegration):
|
||||
script_type = opts.get("script_type", "sh")
|
||||
arg_placeholder = "$ARGUMENTS"
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
processed = self.process_template(
|
||||
raw, self.key, script_type, arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
)
|
||||
dst_name = self.command_filename(src_file.stem)
|
||||
dst_file = self.write_file_and_record(
|
||||
@@ -130,5 +133,7 @@ class GenericIntegration(MarkdownIntegration):
|
||||
)
|
||||
created.append(dst_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
return created
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Goose integration — open source AI agent (Agentic AI Foundation)."""
|
||||
"""Goose integration — Block's open source AI agent."""
|
||||
|
||||
from ..base import YamlIntegration
|
||||
|
||||
@@ -9,7 +9,7 @@ class GooseIntegration(YamlIntegration):
|
||||
"name": "Goose",
|
||||
"folder": ".goose/",
|
||||
"commands_subdir": "recipes",
|
||||
"install_url": "https://goose-docs.ai/docs/getting-started/installation",
|
||||
"install_url": "https://block.github.io/goose/docs/getting-started/installation",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
@@ -18,3 +18,4 @@ class GooseIntegration(YamlIntegration):
|
||||
"args": "{{args}}",
|
||||
"extension": ".yaml",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@@ -50,6 +50,7 @@ class HermesIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
# -- Helpers -----------------------------------------------------------
|
||||
|
||||
@@ -113,6 +114,7 @@ class HermesIntegration(SkillsIntegration):
|
||||
global_skills_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
created: list[Path] = []
|
||||
context_file_display = self._context_file_display(project_root)
|
||||
|
||||
for src_file in templates:
|
||||
raw = src_file.read_text(encoding="utf-8")
|
||||
@@ -139,6 +141,7 @@ class HermesIntegration(SkillsIntegration):
|
||||
self.key,
|
||||
script_type,
|
||||
arg_placeholder,
|
||||
context_file=context_file_display,
|
||||
invoke_separator=self.invoke_separator,
|
||||
)
|
||||
# Strip the processed frontmatter — we rebuild it for skills.
|
||||
@@ -180,6 +183,8 @@ class HermesIntegration(SkillsIntegration):
|
||||
skill_file.write_bytes(normalized.encode("utf-8"))
|
||||
created.append(skill_file)
|
||||
|
||||
# Upsert managed context section into the agent context file
|
||||
self.upsert_context_section(project_root)
|
||||
|
||||
# Create project-local marker directory so extension commands
|
||||
# (e.g. git) can detect Hermes as an active integration.
|
||||
@@ -199,7 +204,8 @@ class HermesIntegration(SkillsIntegration):
|
||||
) -> tuple[list[Path], list[Path]]:
|
||||
"""Uninstall integration files including global Hermes skills.
|
||||
|
||||
Removes the project-local marker directory (if empty), delegates to
|
||||
Removes the managed context section from AGENTS.md, removes the
|
||||
project-local marker directory (if empty), delegates to
|
||||
``manifest.uninstall()`` for project-local tracked files, and
|
||||
removes all ``speckit-*`` skills under ``~/.hermes/skills/``.
|
||||
|
||||
@@ -207,6 +213,8 @@ class HermesIntegration(SkillsIntegration):
|
||||
standard integration behaviour where all files created by the
|
||||
integration are removed on ``specify integration uninstall``.
|
||||
"""
|
||||
# Remove managed context section from AGENTS.md
|
||||
self.remove_context_section(project_root)
|
||||
|
||||
# Delegate to manifest for project-local tracked files (scripts,
|
||||
# templates, context entries tracked in the manifest).
|
||||
|
||||
22
src/specify_cli/integrations/iflow/__init__.py
Normal file
22
src/specify_cli/integrations/iflow/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""iFlow CLI integration."""
|
||||
|
||||
from ..base import MarkdownIntegration
|
||||
|
||||
|
||||
class IflowIntegration(MarkdownIntegration):
|
||||
key = "iflow"
|
||||
config = {
|
||||
"name": "iFlow CLI",
|
||||
"folder": ".iflow/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": "https://docs.iflow.cn/en/cli/quickstart",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".iflow/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "IFLOW.md"
|
||||
multi_install_safe = True
|
||||
@@ -18,4 +18,5 @@ class JunieIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = ".junie/AGENTS.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@@ -18,4 +18,5 @@ class KilocodeIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = ".kilocode/rules/specify-rules.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Kimi Code integration — skills-based agent (Moonshot AI).
|
||||
|
||||
Kimi uses the ``.kimi-code/skills/speckit-<name>/SKILL.md`` layout with
|
||||
Kimi uses the ``.kimi/skills/speckit-<name>/SKILL.md`` layout with
|
||||
``/skill:speckit-<name>`` invocation syntax.
|
||||
|
||||
Legacy migration covers projects created before Kimi Code CLI moved to
|
||||
this layout and handles two distinct changes: the directory move from
|
||||
``.kimi/`` to ``.kimi-code/``, and the dotted-to-hyphenated skill naming
|
||||
(``speckit.xxx`` → ``speckit-xxx``).
|
||||
Includes legacy migration logic for projects initialised before Kimi
|
||||
moved from dotted skill directories (``speckit.xxx``) to hyphenated
|
||||
(``speckit-xxx``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,42 +24,19 @@ class KimiIntegration(SkillsIntegration):
|
||||
key = "kimi"
|
||||
config = {
|
||||
"name": "Kimi Code",
|
||||
"folder": ".kimi-code/",
|
||||
"folder": ".kimi/",
|
||||
"commands_subdir": "skills",
|
||||
"install_url": "https://code.kimi.com/",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".kimi-code/skills",
|
||||
"dir": ".kimi/skills",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
multi_install_safe = False
|
||||
|
||||
def build_command_invocation(self, command_name: str, args: str = "") -> str:
|
||||
"""Build Kimi's native skill invocation: ``/skill:speckit-<stem>``.
|
||||
|
||||
Kimi Code CLI invokes installed skills with a ``/skill:<name>``
|
||||
slash command (e.g. ``/skill:speckit-plan``), not the bare
|
||||
``/speckit-<name>`` form produced by the generic skills base
|
||||
class. Overriding here keeps ``dispatch_command()`` and workflow
|
||||
command steps aligned with the ``/skill:`` guidance shown at init
|
||||
time and in rendered hook invocations.
|
||||
"""
|
||||
stem = command_name
|
||||
if stem.startswith("speckit."):
|
||||
stem = stem[len("speckit.") :]
|
||||
|
||||
invocation = "/skill:speckit-" + stem.replace(".", "-")
|
||||
if args:
|
||||
invocation = f"{invocation} {args}"
|
||||
return invocation
|
||||
|
||||
def post_process_skill_content(self, content: str) -> str:
|
||||
"""Ensure in-skill cross-command references use Kimi's `/skill:` syntax."""
|
||||
content = super().post_process_skill_content(content)
|
||||
return content.replace("/speckit-", "/skill:speckit-")
|
||||
context_file = "KIMI.md"
|
||||
multi_install_safe = True
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
@@ -75,10 +51,7 @@ class KimiIntegration(SkillsIntegration):
|
||||
"--migrate-legacy",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help=(
|
||||
"Migrate legacy Kimi installations: "
|
||||
".kimi/skills/ → .kimi-code/skills/ and speckit.xxx → speckit-xxx"
|
||||
),
|
||||
help="Migrate legacy dotted skill dirs (speckit.xxx → speckit-xxx)",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -89,283 +62,64 @@ class KimiIntegration(SkillsIntegration):
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
**opts: Any,
|
||||
) -> list[Path]:
|
||||
"""Install skills with optional legacy migration."""
|
||||
"""Install skills with optional legacy dotted-name migration."""
|
||||
parsed_options = parsed_options or {}
|
||||
|
||||
# Refuse a symlinked destination before any writes occur. base
|
||||
# setup() only rejects a destination that *escapes* project_root
|
||||
# after resolve(), so an in-tree symlinked ``.kimi-code`` /
|
||||
# ``.kimi-code/skills`` (e.g. ``-> .``) would still pass that check
|
||||
# and misdirect the SKILL.md writes into an unintended in-tree
|
||||
# location (e.g. ``./skills/``). Reject any symlinked destination
|
||||
# component up front so this never happens.
|
||||
new_skills_dir = self.skills_dest(project_root)
|
||||
if _has_symlinked_component(new_skills_dir, project_root):
|
||||
raise ValueError(
|
||||
f"Skills destination {new_skills_dir} contains a symlinked "
|
||||
f"path component; refusing to install into it."
|
||||
)
|
||||
|
||||
# Run base setup first so new-path targets (speckit-*) exist,
|
||||
# then migrate/clean legacy dirs without risking user content loss.
|
||||
# Run base setup first so hyphenated targets (speckit-*) exist,
|
||||
# then migrate/clean legacy dotted dirs without risking user content loss.
|
||||
created = super().setup(
|
||||
project_root, manifest, parsed_options=parsed_options, **opts
|
||||
)
|
||||
|
||||
if parsed_options.get("migrate_legacy", False):
|
||||
old_skills_dir = project_root / ".kimi" / "skills"
|
||||
# Validate both endpoints. base setup() already rejects a
|
||||
# destination that *escapes* the project root, but an in-tree
|
||||
# symlinked ``.kimi-code``/``.kimi-code/skills`` (e.g. ``-> .``)
|
||||
# would still misdirect the move; ``_is_safe_legacy_dir`` rejects
|
||||
# any symlinked component, giving the destination the same
|
||||
# protection as the source.
|
||||
if _is_safe_legacy_dir(old_skills_dir, project_root) and (
|
||||
_is_safe_legacy_dir(new_skills_dir, project_root)
|
||||
):
|
||||
_migrate_legacy_kimi_skills_dir(old_skills_dir, new_skills_dir)
|
||||
skills_dir = self.skills_dest(project_root)
|
||||
if skills_dir.is_dir():
|
||||
_migrate_legacy_kimi_dotted_skills(skills_dir)
|
||||
|
||||
return created
|
||||
|
||||
def teardown(
|
||||
self,
|
||||
project_root: Path,
|
||||
manifest: IntegrationManifest,
|
||||
*,
|
||||
force: bool = False,
|
||||
) -> tuple[list[Path], list[Path]]:
|
||||
"""Uninstall Kimi skills and remove leftover legacy directories."""
|
||||
removed, skipped = super().teardown(project_root, manifest, force=force)
|
||||
|
||||
old_skills_dir = project_root / ".kimi" / "skills"
|
||||
if _is_safe_legacy_dir(old_skills_dir, project_root):
|
||||
legacy_dirs = sorted(
|
||||
[*old_skills_dir.glob("speckit-*"), *old_skills_dir.glob("speckit.*")]
|
||||
)
|
||||
for legacy_dir in legacy_dirs:
|
||||
if legacy_dir.is_symlink() or not legacy_dir.is_dir():
|
||||
continue
|
||||
if _is_speckit_generated_skill(legacy_dir):
|
||||
try:
|
||||
shutil.rmtree(legacy_dir)
|
||||
removed.append(legacy_dir)
|
||||
except OSError:
|
||||
skipped.append(legacy_dir)
|
||||
|
||||
try:
|
||||
old_skills_dir.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return removed, skipped
|
||||
|
||||
|
||||
def _has_symlinked_component(path: Path, project_root: Path) -> bool:
|
||||
"""Return ``True`` when *path* escapes *project_root* or any component is a symlink.
|
||||
|
||||
Walks the components strictly between *project_root* and *path*
|
||||
(including the final one) and reports whether any of them is a symlink.
|
||||
Components that do not exist yet are not symlinks, so this safely handles
|
||||
a not-yet-created destination. *project_root* itself is trusted and never
|
||||
checked. A *path* outside *project_root* is treated as unsafe.
|
||||
"""
|
||||
try:
|
||||
relative = path.relative_to(project_root)
|
||||
except ValueError:
|
||||
return True
|
||||
current = project_root
|
||||
for part in relative.parts:
|
||||
current = current / part
|
||||
if current.is_symlink():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _is_safe_legacy_dir(path: Path, project_root: Path) -> bool:
|
||||
"""Return ``True`` when *path* is a real directory safely inside *project_root*.
|
||||
|
||||
Legacy migration and cleanup ``shutil.move()`` and ``shutil.rmtree()``
|
||||
directories, so a symlinked ``.kimi``/``.kimi/skills`` (or one reached
|
||||
through a symlinked parent) must never be followed: doing so could
|
||||
relocate or delete content living outside the project tree — or operate
|
||||
on an unrelated in-tree directory (e.g. ``.kimi -> .`` makes
|
||||
``.kimi/skills`` resolve to ``./skills``).
|
||||
|
||||
Checking only the fully-resolved path is insufficient, because a symlink
|
||||
pointing elsewhere *inside* the project still resolves to a location under
|
||||
*project_root*. We therefore reject the path when it is not a directory,
|
||||
when any component between *project_root* and *path* is a symlink
|
||||
(including the final component), or when the resolved path escapes the
|
||||
resolved *project_root*.
|
||||
"""
|
||||
if not path.is_dir():
|
||||
return False
|
||||
|
||||
# Reject if any path component below project_root is a symlink (or the
|
||||
# path escapes project_root). We trust project_root itself, so only
|
||||
# components strictly under it are checked.
|
||||
if _has_symlinked_component(path, project_root):
|
||||
return False
|
||||
|
||||
try:
|
||||
resolved = path.resolve()
|
||||
root = project_root.resolve()
|
||||
except OSError:
|
||||
return False
|
||||
return resolved == root or root in resolved.parents
|
||||
|
||||
|
||||
def _migrate_legacy_kimi_skills_dir(
|
||||
old_skills_dir: Path, new_skills_dir: Path
|
||||
) -> tuple[int, int]:
|
||||
"""Migrate skills from the legacy ``.kimi/skills/`` directory to ``.kimi-code/skills/``.
|
||||
|
||||
Handles both hyphenated (``speckit-xxx``) and dotted (``speckit.xxx``)
|
||||
legacy directory names. If a target already exists, the legacy dir is
|
||||
only removed when its ``SKILL.md`` is byte-identical and no extra user
|
||||
files are present.
|
||||
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
|
||||
"""Migrate legacy Kimi dotted skill dirs (speckit.xxx) to hyphenated format.
|
||||
|
||||
Returns ``(migrated_count, removed_count)``.
|
||||
"""
|
||||
if not old_skills_dir.is_dir():
|
||||
if not skills_dir.is_dir():
|
||||
return (0, 0)
|
||||
|
||||
migrated_count = 0
|
||||
removed_count = 0
|
||||
|
||||
# Process hyphenated dirs first, then dotted dirs.
|
||||
legacy_dirs = sorted(old_skills_dir.glob("speckit-*")) + sorted(
|
||||
old_skills_dir.glob("speckit.*")
|
||||
)
|
||||
|
||||
for legacy_dir in legacy_dirs:
|
||||
if legacy_dir.is_symlink() or not legacy_dir.is_dir():
|
||||
for legacy_dir in sorted(skills_dir.glob("speckit.*")):
|
||||
if not legacy_dir.is_dir():
|
||||
continue
|
||||
legacy_skill = legacy_dir / "SKILL.md"
|
||||
# Treat a symlinked SKILL.md as invalid: later read_bytes() would
|
||||
# otherwise follow it and read content from outside the project.
|
||||
if legacy_skill.is_symlink() or not legacy_skill.is_file():
|
||||
if not (legacy_dir / "SKILL.md").exists():
|
||||
continue
|
||||
|
||||
target_name = _legacy_to_target_name(legacy_dir.name)
|
||||
if not target_name:
|
||||
suffix = legacy_dir.name[len("speckit."):]
|
||||
if not suffix:
|
||||
continue
|
||||
|
||||
target_dir = new_skills_dir / target_name
|
||||
|
||||
# Skip if the legacy dir is already the target dir (same-directory call).
|
||||
if legacy_dir.resolve() == target_dir.resolve():
|
||||
continue
|
||||
target_dir = skills_dir / f"speckit-{suffix.replace('.', '-')}"
|
||||
|
||||
if not target_dir.exists():
|
||||
target_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(legacy_dir), str(target_dir))
|
||||
migrated_count += 1
|
||||
continue
|
||||
|
||||
# Target exists — only remove legacy if SKILL.md is identical.
|
||||
# Skip when the target dir or its SKILL.md is a symlink (or the dir is
|
||||
# not a real directory) so the byte comparison never follows a link
|
||||
# outside the project. (legacy_skill is already guaranteed to be a real
|
||||
# file by the guard above.)
|
||||
if target_dir.is_symlink() or not target_dir.is_dir():
|
||||
continue
|
||||
# Target exists — only remove legacy if SKILL.md is identical
|
||||
target_skill = target_dir / "SKILL.md"
|
||||
if target_skill.is_symlink() or not target_skill.is_file():
|
||||
continue
|
||||
try:
|
||||
if target_skill.read_bytes() == legacy_skill.read_bytes():
|
||||
has_extra = any(
|
||||
child.name != "SKILL.md" for child in legacy_dir.iterdir()
|
||||
)
|
||||
if not has_extra:
|
||||
shutil.rmtree(legacy_dir)
|
||||
removed_count += 1
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Remove the legacy skills directory if it is now empty.
|
||||
try:
|
||||
old_skills_dir.rmdir()
|
||||
except OSError:
|
||||
pass
|
||||
legacy_skill = legacy_dir / "SKILL.md"
|
||||
if target_skill.is_file():
|
||||
try:
|
||||
if target_skill.read_bytes() == legacy_skill.read_bytes():
|
||||
has_extra = any(
|
||||
child.name != "SKILL.md" for child in legacy_dir.iterdir()
|
||||
)
|
||||
if not has_extra:
|
||||
shutil.rmtree(legacy_dir)
|
||||
removed_count += 1
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return (migrated_count, removed_count)
|
||||
|
||||
|
||||
def _legacy_to_target_name(legacy_name: str) -> str:
|
||||
"""Convert a legacy skill directory name to the modern hyphenated form."""
|
||||
if legacy_name.startswith("speckit-"):
|
||||
return legacy_name
|
||||
if legacy_name.startswith("speckit."):
|
||||
suffix = legacy_name[len("speckit.") :]
|
||||
if suffix:
|
||||
return f"speckit-{suffix.replace('.', '-')}"
|
||||
return ""
|
||||
|
||||
|
||||
def _is_speckit_generated_skill(skill_dir: Path) -> bool:
|
||||
"""Return True when *skill_dir* contains a Speckit-generated SKILL.md.
|
||||
|
||||
Uses the ``metadata.author`` and ``metadata.source`` fields written by
|
||||
``SkillsIntegration.setup()`` to avoid deleting user-authored skills.
|
||||
"""
|
||||
skill_file = skill_dir / "SKILL.md"
|
||||
# A symlinked SKILL.md is never treated as Speckit-generated, so teardown
|
||||
# cleanup never follows it to read frontmatter from outside the project.
|
||||
if skill_file.is_symlink() or not skill_file.is_file():
|
||||
return False
|
||||
|
||||
try:
|
||||
content = skill_file.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
if not content.startswith("---"):
|
||||
return False
|
||||
|
||||
parts = content.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
return False
|
||||
|
||||
try:
|
||||
import yaml
|
||||
|
||||
frontmatter = yaml.safe_load(parts[1])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not isinstance(frontmatter, dict):
|
||||
return False
|
||||
|
||||
metadata = frontmatter.get("metadata", {})
|
||||
if not isinstance(metadata, dict):
|
||||
return False
|
||||
|
||||
author = metadata.get("author", "")
|
||||
source = metadata.get("source", "")
|
||||
return (
|
||||
author == "github-spec-kit"
|
||||
and isinstance(source, str)
|
||||
and source.startswith("templates/commands/")
|
||||
)
|
||||
|
||||
|
||||
def _migrate_legacy_kimi_dotted_skills(skills_dir: Path) -> tuple[int, int]:
|
||||
"""Compatibility shim — migrate legacy dotted skill dirs in place.
|
||||
|
||||
.. deprecated::
|
||||
Kept for direct callers/tests. New code should call
|
||||
``_migrate_legacy_kimi_skills_dir`` directly.
|
||||
|
||||
Delegates to ``_migrate_legacy_kimi_skills_dir`` with *skills_dir* as both
|
||||
source and destination, so it processes every ``speckit-*`` and
|
||||
``speckit.*`` entry under *skills_dir*. Because the two paths are
|
||||
identical, the same-path short-circuit there skips any directory whose
|
||||
target resolves to itself; in practice this renames dotted
|
||||
``speckit.xxx`` dirs to hyphenated ``speckit-xxx`` in place and never
|
||||
moves content outside *skills_dir*.
|
||||
|
||||
Returns ``(migrated_count, removed_count)``.
|
||||
"""
|
||||
return _migrate_legacy_kimi_skills_dir(skills_dir, skills_dir)
|
||||
|
||||
@@ -26,3 +26,4 @@ class KiroCliIntegration(MarkdownIntegration):
|
||||
"args": _KIRO_ARG_FALLBACK,
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@@ -27,6 +27,7 @@ class LingmaIntegration(SkillsIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": "/SKILL.md",
|
||||
}
|
||||
context_file = ".lingma/rules/specify-rules.md"
|
||||
|
||||
@classmethod
|
||||
def options(cls) -> list[IntegrationOption]:
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Oh My Pi (omp) coding agent integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..base import MarkdownIntegration
|
||||
|
||||
|
||||
class OmpIntegration(MarkdownIntegration):
|
||||
key = "omp"
|
||||
config = {
|
||||
"name": "Oh My Pi",
|
||||
"folder": ".omp/",
|
||||
"commands_subdir": "commands",
|
||||
"install_url": "https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".omp/commands",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
|
||||
def build_exec_args(
|
||||
self,
|
||||
prompt: str,
|
||||
*,
|
||||
model: str | None = None,
|
||||
output_json: bool = True,
|
||||
) -> list[str] | None:
|
||||
# Diverges from MarkdownIntegration.build_exec_args because OMP's
|
||||
# CLI parser treats `-p`/`--print` as a boolean (one-shot mode) and
|
||||
# consumes the prompt as a positional argument — see args.ts in
|
||||
# can1357/oh-my-pi. JSON output is selected via `--mode json`.
|
||||
if not self.config or not self.config.get("requires_cli"):
|
||||
return None
|
||||
args = [self._resolve_executable(), "--print"]
|
||||
self._apply_extra_args_env_var(args)
|
||||
if model:
|
||||
args.extend(["--model", model])
|
||||
if output_json:
|
||||
args.extend(["--mode", "json"])
|
||||
args.append(prompt)
|
||||
return args
|
||||
@@ -19,6 +19,7 @@ class OpencodeIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
def build_exec_args(
|
||||
self,
|
||||
|
||||
@@ -9,7 +9,7 @@ class PiIntegration(MarkdownIntegration):
|
||||
"name": "Pi Coding Agent",
|
||||
"folder": ".pi/",
|
||||
"commands_subdir": "prompts",
|
||||
"install_url": "https://www.npmjs.com/package/@earendil-works/pi-coding-agent",
|
||||
"install_url": "https://www.npmjs.com/package/@mariozechner/pi-coding-agent",
|
||||
"requires_cli": True,
|
||||
}
|
||||
registrar_config = {
|
||||
@@ -18,3 +18,4 @@ class PiIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "AGENTS.md"
|
||||
|
||||
@@ -18,4 +18,5 @@ class QodercliIntegration(MarkdownIntegration):
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
}
|
||||
context_file = "QODER.md"
|
||||
multi_install_safe = True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user