mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de4e943f5 |
@@ -65,8 +65,7 @@
|
||||
},
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
".specify/scripts/bash/": true,
|
||||
".specify/scripts/powershell/": true,
|
||||
".specify/scripts/python/": true
|
||||
".specify/scripts/powershell/": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,17 +97,6 @@ echo -e "\n🤖 Installing CodeBuddy CLI..."
|
||||
run_command "npm install -g @tencent-ai/codebuddy-code@latest"
|
||||
echo "✅ Done"
|
||||
|
||||
echo -e "\n🤖 Installing Factory Droid CLI..."
|
||||
run_command "npm install -g droid@latest"
|
||||
|
||||
if ! command -v droid >/dev/null 2>&1; then
|
||||
echo -e "\033[0;31m[ERROR] Droid CLI installation did not create 'droid' in PATH.\033[0m" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_command "droid --version > /dev/null"
|
||||
echo "✅ Done"
|
||||
|
||||
# Installing UV (Python package manager)
|
||||
echo -e "\n🐍 Installing UV - Python Package Manager..."
|
||||
run_command "pipx install uv"
|
||||
|
||||
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**: Alquimia AI, Amp, Antigravity, Auggie CLI, Claude Code, Cline, CodeBuddy, Codex CLI, Cursor, Devin for Terminal, Factory Droid, Firebender, Forge, Gemini CLI, GitHub Copilot, Goose, Grok Build, 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, Grok Build, 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
|
||||
|
||||
- type: input
|
||||
id: agent-name
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -62,7 +62,6 @@ body:
|
||||
label: AI Agent
|
||||
description: Which AI agent are you using?
|
||||
options:
|
||||
- Alquimia AI
|
||||
- Amp
|
||||
- Antigravity
|
||||
- Auggie CLI
|
||||
@@ -72,7 +71,6 @@ body:
|
||||
- Codex CLI
|
||||
- Cursor
|
||||
- Devin for Terminal
|
||||
- Factory Droid
|
||||
- Firebender
|
||||
- Forge
|
||||
- Gemini CLI
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -56,7 +56,6 @@ body:
|
||||
description: Does this feature relate to a specific AI agent?
|
||||
options:
|
||||
- All agents
|
||||
- Alquimia AI
|
||||
- Amp
|
||||
- Antigravity
|
||||
- Auggie CLI
|
||||
@@ -66,7 +65,6 @@ body:
|
||||
- Codex CLI
|
||||
- Cursor
|
||||
- Devin for Terminal
|
||||
- Factory Droid
|
||||
- Firebender
|
||||
- Forge
|
||||
- Gemini CLI
|
||||
|
||||
12
.github/scripts/check_security_requirements.py
vendored
12
.github/scripts/check_security_requirements.py
vendored
@@ -29,20 +29,12 @@ def _dependency_diff_refs() -> tuple[str, str]:
|
||||
def _dependency_inputs_changed() -> bool:
|
||||
base_ref, head_ref = _dependency_diff_refs()
|
||||
try:
|
||||
merge_base = subprocess.run(
|
||||
["git", "merge-base", base_ref, head_ref],
|
||||
check=True,
|
||||
cwd=REPO_ROOT,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--name-only",
|
||||
merge_base,
|
||||
base_ref,
|
||||
head_ref,
|
||||
"--",
|
||||
*DEPENDENCY_INPUTS,
|
||||
@@ -85,7 +77,6 @@ def main() -> int:
|
||||
|
||||
generated_requirements = Path(generated_requirements_env)
|
||||
generated_requirements.parent.mkdir(parents=True, exist_ok=True)
|
||||
generated_requirements.write_bytes(COMMITTED_REQUIREMENTS.read_bytes())
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
@@ -96,6 +87,7 @@ def main() -> int:
|
||||
"--extra",
|
||||
"test",
|
||||
"--universal",
|
||||
"--upgrade",
|
||||
"--generate-hashes",
|
||||
"--quiet",
|
||||
"--no-header",
|
||||
|
||||
6
.github/security-audit-requirements.txt
vendored
6
.github/security-audit-requirements.txt
vendored
@@ -1,6 +1,6 @@
|
||||
annotated-doc==0.0.5 \
|
||||
--hash=sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101 \
|
||||
--hash=sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb
|
||||
annotated-doc==0.0.4 \
|
||||
--hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \
|
||||
--hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4
|
||||
# via typer
|
||||
click==8.4.2 \
|
||||
--hash=sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6 \
|
||||
|
||||
16
.github/workflows/add-community-bundle.lock.yml
generated
vendored
16
.github/workflows/add-community-bundle.lock.yml
generated
vendored
@@ -33,10 +33,10 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# - actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
# - github/gh-aw-actions/setup@c0338fef4749d08c21f8f975fb0e37efa17dda47 # v0.79.8
|
||||
#
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1352,7 +1352,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
@@ -1419,7 +1419,7 @@ jobs:
|
||||
mkdir -p /tmp/gh-aw/threat-detection
|
||||
touch /tmp/gh-aw/threat-detection/detection.log
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: '24'
|
||||
package-manager-cache: false
|
||||
@@ -1678,7 +1678,7 @@ jobs:
|
||||
await main();
|
||||
- name: Checkout repository (trusted default branch for comment events)
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
@@ -1686,7 +1686,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Checkout repository
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
12
.github/workflows/add-community-extension.lock.yml
generated
vendored
12
.github/workflows/add-community-extension.lock.yml
generated
vendored
@@ -33,7 +33,7 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1332,7 +1332,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
@@ -1658,7 +1658,7 @@ jobs:
|
||||
await main();
|
||||
- name: Checkout repository (trusted default branch for comment events)
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
@@ -1666,7 +1666,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Checkout repository
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
12
.github/workflows/add-community-preset.lock.yml
generated
vendored
12
.github/workflows/add-community-preset.lock.yml
generated
vendored
@@ -33,7 +33,7 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1332,7 +1332,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
@@ -1658,7 +1658,7 @@ jobs:
|
||||
await main();
|
||||
- name: Checkout repository (trusted default branch for comment events)
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
@@ -1666,7 +1666,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Checkout repository
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
8
.github/workflows/bug-assess.lock.yml
generated
vendored
8
.github/workflows/bug-assess.lock.yml
generated
vendored
@@ -32,7 +32,7 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -430,7 +430,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1277,7 +1277,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
|
||||
12
.github/workflows/bug-fix.lock.yml
generated
vendored
12
.github/workflows/bug-fix.lock.yml
generated
vendored
@@ -33,7 +33,7 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
@@ -162,7 +162,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -434,7 +434,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1338,7 +1338,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
@@ -1664,7 +1664,7 @@ jobs:
|
||||
await main();
|
||||
- name: Checkout repository (trusted default branch for comment events)
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment')
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
@@ -1672,7 +1672,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Checkout repository
|
||||
if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') && github.event_name != 'issue_comment' && github.event_name != 'pull_request_review_comment'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ steps.extract-base-branch.outputs.base-branch || github.base_ref || github.event.pull_request.base.ref || github.ref_name || github.event.repository.default_branch }}
|
||||
token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
8
.github/workflows/bug-test.lock.yml
generated
vendored
8
.github/workflows/bug-test.lock.yml
generated
vendored
@@ -32,7 +32,7 @@
|
||||
# - GITHUB_TOKEN
|
||||
#
|
||||
# Custom actions used:
|
||||
# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
- name: Checkout .github and .agents folders
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
sparse-checkout: |
|
||||
@@ -431,7 +431,7 @@ jobs:
|
||||
echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -1299,7 +1299,7 @@ jobs:
|
||||
echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
|
||||
- name: Checkout repository for patch context
|
||||
if: needs.agent.outputs.has_patch == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
# --- Threat Detection ---
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -19,14 +19,14 @@ jobs:
|
||||
language: [ 'actions', 'python' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history for git info
|
||||
|
||||
|
||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@6bf21b07787794f89a243495939cd651942aeabe # v24.1.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@8de2aa07cae85fd17c0b35642db70cf5495f1d25 # v24.0.0
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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
|
||||
|
||||
8
.github/workflows/publish-pypi.yml
vendored
8
.github/workflows/publish-pypi.yml
vendored
@@ -27,15 +27,15 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: refs/tags/${{ inputs.tag }}
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
path: dist/
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Publish to PyPI
|
||||
run: uv publish
|
||||
|
||||
2
.github/workflows/release-trigger.yml
vendored
2
.github/workflows/release-trigger.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
14
.github/workflows/security.yml
vendored
14
.github/workflows/security.yml
vendored
@@ -19,22 +19,22 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Check committed audit requirements are current
|
||||
env:
|
||||
DEPENDENCY_DIFF_BASE: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
|
||||
DEPENDENCY_DIFF_HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
DEPENDENCY_DIFF_HEAD: ${{ github.sha }}
|
||||
GENERATED_REQUIREMENTS: ${{ runner.temp }}/security-audit-requirements.txt
|
||||
run: python .github/scripts/check_security_requirements.py
|
||||
|
||||
@@ -52,13 +52,13 @@ jobs:
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
||||
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10
|
||||
with:
|
||||
# Days of inactivity before an issue or PR becomes stale
|
||||
days-before-stale: 150
|
||||
|
||||
14
.github/workflows/test.yml
vendored
14
.github/workflows/test.yml
vendored
@@ -13,18 +13,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
with:
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Run ruff check
|
||||
run: uvx ruff@0.15.0 check src tests
|
||||
run: uvx ruff check src tests
|
||||
|
||||
pytest:
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -34,13 +34,13 @@ jobs:
|
||||
python-version: ["3.13", "3.14"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
|
||||
142
AGENTS.md
142
AGENTS.md
@@ -10,20 +10,6 @@ The toolkit supports multiple AI coding assistants, allowing teams to use their
|
||||
|
||||
---
|
||||
|
||||
## Quickstart — Add a New Integration in 5 Steps
|
||||
|
||||
If you are new to the codebase and want to add support for a new AI agent, here is the shortest path from zero to a working integration:
|
||||
|
||||
1. **Choose a base class** — most agents only need `MarkdownIntegration`. See [Choose a base class](#1-choose-a-base-class).
|
||||
2. **Create a subpackage** — add `src/specify_cli/integrations/<package_dir>/__init__.py` with the required `key`, `config`, and `registrar_config` fields.
|
||||
3. **Register it** — add one import and one `_register()` call in `src/specify_cli/integrations/__init__.py` (both alphabetical).
|
||||
4. **Write a test file** — create `tests/integrations/test_integration_<key>.py` (hyphens in the key become underscores in the filename).
|
||||
5. **Run and verify** — use `specify init --integration <key>` to exercise the full install/uninstall cycle.
|
||||
|
||||
Each step is expanded under [Adding a New Integration](#adding-a-new-integration). Note that agent **context files** (`CLAUDE.md`, `AGENTS.md`, …) are **not** handled by the integration — that is owned by the opt-in `agent-context` extension; see [Context file behavior](#4-context-file-behavior).
|
||||
|
||||
---
|
||||
|
||||
## Integration Architecture
|
||||
|
||||
Each AI agent is a self-contained **integration subpackage** under `src/specify_cli/integrations/<key>/`. The subpackage exposes a single class that declares all metadata and inherits setup/teardown logic from a base class. Built-in integrations are then instantiated and added to the global `INTEGRATION_REGISTRY` by `src/specify_cli/integrations/__init__.py` via `_register_builtins()`.
|
||||
@@ -48,30 +34,6 @@ The registry is the **single source of truth for Python integration metadata**.
|
||||
|
||||
---
|
||||
|
||||
## IntegrationManifest — File Tracking
|
||||
|
||||
`manifest.py` provides the `IntegrationManifest` class, which records every file an integration installs. This record is what makes uninstall reliable and safe.
|
||||
|
||||
### How it works
|
||||
|
||||
`setup()` receives an `IntegrationManifest` and writes files through it rather than touching the filesystem directly:
|
||||
|
||||
```python
|
||||
# Produce a new file and record its hash for later verification.
|
||||
manifest.record_file("commands/speckit.plan.md", processed_content)
|
||||
|
||||
# Adopt a pre-existing file the integration is now responsible for.
|
||||
manifest.record_existing(".vscode/settings.json")
|
||||
```
|
||||
|
||||
The manifest is persisted at `.specify/integrations/<key>.manifest.json` (one per integration, keyed by `key`) and stores a SHA-256 hash per file. When the user runs `specify integration uninstall <key>`, `teardown()` delegates to `manifest.uninstall()`, which removes only files whose current hash still matches the recorded value — so files the user later edited by hand are skipped, not clobbered (use `specify integration uninstall <key> --force` to remove modified tracked files anyway).
|
||||
|
||||
### Why this matters
|
||||
|
||||
Without hash-tracked manifests, uninstall would either remove files it should not (destructive) or leave orphans behind (messy). If you write a custom `setup()`, route **every** file you create through `manifest.record_file(...)` (or `record_existing(...)` for files you adopt) so uninstall can reason about them.
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Integration
|
||||
|
||||
### 1. Choose a base class
|
||||
@@ -102,14 +64,13 @@ class KilocodeIntegration(MarkdownIntegration):
|
||||
key = "kilocode"
|
||||
config = {
|
||||
"name": "Kilo Code",
|
||||
"folder": ".kilo/",
|
||||
"commands_subdir": "commands",
|
||||
"folder": ".kilocode/",
|
||||
"commands_subdir": "workflows",
|
||||
"install_url": None,
|
||||
"requires_cli": False,
|
||||
}
|
||||
registrar_config = {
|
||||
"dir": ".kilo/commands",
|
||||
"legacy_dir": ".kilocode/workflows",
|
||||
"dir": ".kilocode/workflows",
|
||||
"format": "markdown",
|
||||
"args": "$ARGUMENTS",
|
||||
"extension": ".md",
|
||||
@@ -226,7 +187,7 @@ 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`, `.ps1`, and `extensions/agent-context/scripts/python/update_agent_context.py`). The CLI registry is never consulted — all agent→context-file knowledge lives inside the extension.
|
||||
- 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.
|
||||
|
||||
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.
|
||||
@@ -240,8 +201,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, .kilo/commands/)
|
||||
ls -R my-project/.kilo/commands/
|
||||
# config["folder"] + config["commands_subdir"] (for example, .kilocode/workflows/)
|
||||
ls -R my-project/.kilocode/workflows/
|
||||
|
||||
# Uninstall cleanly
|
||||
cd my-project && specify integration uninstall <key>
|
||||
@@ -307,25 +268,6 @@ echo "✅ Done"
|
||||
|
||||
## Command File Formats
|
||||
|
||||
### Script References (`scripts:` frontmatter)
|
||||
|
||||
Core command templates (`templates/commands/*.md`) that invoke a helper script declare it in a `scripts:` frontmatter block with one line per supported script type. The `{SCRIPT}` placeholder in the command body is replaced at install time with the entry matching the project's selected script type (`--script sh|ps|py`):
|
||||
|
||||
```yaml
|
||||
scripts:
|
||||
sh: scripts/bash/setup-plan.sh --json
|
||||
ps: scripts/powershell/setup-plan.ps1 -Json
|
||||
py: scripts/python/setup_plan.py --json
|
||||
```
|
||||
|
||||
| Key | Script type | Location |
|
||||
| ---- | ---------------------- | -------------------------- |
|
||||
| `sh` | POSIX shell (bash/zsh) | `scripts/bash/*.sh` |
|
||||
| `ps` | PowerShell | `scripts/powershell/*.ps1` |
|
||||
| `py` | Python | `scripts/python/*.py` |
|
||||
|
||||
All three entries must be present and behaviorally equivalent — agents parse the same stdout contract (`FEATURE_DIR:…`, `AVAILABLE_DOCS:…`, `--json` shapes) regardless of which one runs. (The bundled `agent-context` and `git` extension command templates also invoke helpers but do not yet use `scripts:` frontmatter — see [Script Types and Migration](#script-types-and-migration).)
|
||||
|
||||
### Markdown Format
|
||||
|
||||
**Standard format:**
|
||||
@@ -386,29 +328,9 @@ Different agents use different argument placeholders. The placeholder used in co
|
||||
- **TOML-based**: `{{args}}` (e.g., Gemini)
|
||||
- **YAML-based**: `{{args}}` (e.g., Goose)
|
||||
- **Custom**: some agents override the default (e.g., Forge uses `{{parameters}}`)
|
||||
- **Script placeholders**: `{SCRIPT}` (replaced with the resolved command from the template's `scripts:` frontmatter, per the project's `--script sh|ps|py` selection)
|
||||
- **Script placeholders**: `{SCRIPT}` (replaced with actual script path)
|
||||
- **Agent placeholders**: `__AGENT__` (replaced with agent name)
|
||||
|
||||
## Script Types and Migration
|
||||
|
||||
Spec Kit ships every core workflow script in three interchangeable variants — POSIX shell (`sh`), PowerShell (`ps`), and Python (`py`) — selected per project with `specify init --script sh|ps|py`. Each core command template that invokes a helper script carries all three in its `scripts:` frontmatter (templates that don't call a script, e.g. `constitution`/`specify`, have no `scripts:` block); see [Script References](#script-references-scripts-frontmatter).
|
||||
|
||||
### Why Python is recommended
|
||||
|
||||
- **No extra runtime.** The `specify` CLI is already Python, so the interpreter is guaranteed present — `py` adds no new dependency.
|
||||
- **Path toward a single source of truth.** The shell variants require paired `.sh` + `.ps1` maintenance and diverge on JSON handling (`jq` vs manual parsing). The Python variant avoids `jq` and is intended to eventually replace that dual-maintenance — but that consolidation has not happened yet: all three variants are still maintained in parallel (see the parity rule below).
|
||||
- **Parity-tested.** The Python ports are covered by tests — output-parity tests against the shell scripts where the contract is stdout-based, and direct unit tests elsewhere — so the stdout contract agents rely on stays stable.
|
||||
|
||||
### Defaults and availability
|
||||
|
||||
- `py` is available today for the core command templates (via their `scripts:` frontmatter). The bundled extensions (`agent-context`, `git`) ship Python script variants on disk, but their command templates still hard-code the Bash/PowerShell invocations, so `--script py` does not yet route those extension commands to Python — wiring `py` into the extension command templates is tracked separately.
|
||||
- Selection is per project: interactive `specify init` prompts for the script type, while non-interactive runs default to a shell variant by OS (`sh` on Linux/macOS, `ps` on Windows). `py` is chosen at the prompt or via `--script py`.
|
||||
- `sh` and `ps` remain fully supported. Nothing is removed, and `py` is not yet the default.
|
||||
|
||||
### Parity rule for contributors
|
||||
|
||||
All three script types are first-class: any change to a workflow script must update `sh`, `ps`, and `py` together and keep their tests (parity and unit) green. Making `py` the default and eventually retiring `sh`/`ps` is future work gated on adoption, tracked under the script-unification epic ([#3277](https://github.com/github/spec-kit/issues/3277)) — not something to act on from this doc.
|
||||
|
||||
## Special Processing Requirements
|
||||
|
||||
Some agents require custom processing beyond the standard template transformations:
|
||||
@@ -549,54 +471,4 @@ Disclosure is **continuous**, not a one-time event. A single AI-disclosure parag
|
||||
|
||||
---
|
||||
|
||||
## Error Handling and Debugging
|
||||
|
||||
### Common Errors and Fixes
|
||||
|
||||
| Symptom | Likely Cause | Fix |
|
||||
|---|---|---|
|
||||
| `Integration '<key>' not found` | Missing `_register()` call | Add `_register(<Name>Integration())` inside `_register_builtins()` |
|
||||
| `NameError: name '<Name>Integration' is not defined` at startup | Missing import | Add `from .<package_dir> import <Name>Integration` inside `_register_builtins()` |
|
||||
| CLI check fails for a `requires_cli: True` agent | `key` does not match the executable name | Set `key` to the exact name `shutil.which(key)` must resolve (e.g. `"cursor-agent"`, not `"cursor"`) |
|
||||
| Command files have the wrong argument syntax | Wrong `args` value in `registrar_config` | Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML/YAML agents, or the agent's custom placeholder |
|
||||
| `ModuleNotFoundError` on a brand-new subpackage under pytest only | Ambient interpreter with a stale editable `.pth` | Run inside this tree's own venv (see Common Pitfall 6) |
|
||||
| Uninstall leaves files behind, or skips files you expected removed | Files not recorded via the manifest, or their hash changed after install | Route every created file through `manifest.record_file(...)`; user-edited files are intentionally skipped unless `force=True` |
|
||||
| Context file (`CLAUDE.md`, etc.) not updated | Expecting the CLI to manage it | Context files are owned by the opt-in `agent-context` extension, not the integration — see [Context file behavior](#4-context-file-behavior) |
|
||||
|
||||
### Debugging Tips
|
||||
|
||||
**Inspect the manifest** to see what an installed integration tracks:
|
||||
|
||||
```bash
|
||||
cat .specify/integrations/<key>.manifest.json
|
||||
```
|
||||
|
||||
**Verify a CLI tool is detected** before debugging a `requires_cli` agent:
|
||||
|
||||
```bash
|
||||
which <key> # Should print the executable path if installed
|
||||
```
|
||||
|
||||
**Verify the installed output structure** after `specify init`:
|
||||
|
||||
```bash
|
||||
find my-project/<folder> -type f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contribution Checklist
|
||||
|
||||
Before opening or merging an integration PR, confirm the following:
|
||||
|
||||
- [ ] Added the integration subpackage under `src/specify_cli/integrations/<package_dir>/`.
|
||||
- [ ] Registered it (import **and** `_register()`) in `src/specify_cli/integrations/__init__.py`, both alphabetical.
|
||||
- [ ] Added or updated tests in `tests/integrations/test_integration_<key>.py`.
|
||||
- [ ] Verified the install/uninstall flow with `specify init --integration <key>`.
|
||||
- [ ] Did **not** add `context_file` handling to the CLI (that belongs to the `agent-context` extension).
|
||||
- [ ] Updated devcontainer files if the agent needs a VS Code extension or CLI install step.
|
||||
- [ ] Updated this guide or other relevant docs if the integration has special setup or limitations.
|
||||
|
||||
---
|
||||
|
||||
*This documentation should be updated whenever new integrations are added to maintain accuracy and completeness.*
|
||||
|
||||
218
CHANGELOG.md
218
CHANGELOG.md
@@ -2,224 +2,6 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.15.1] - 2026-07-31
|
||||
|
||||
### Changed
|
||||
|
||||
- fix: escape Rich markup in `workflow resolve` output (#3879)
|
||||
- chore(deps): bump actions/stale from 10.4.0 to 11.0.0 (#3877)
|
||||
- chore(deps): bump actions/setup-python from 6.3.0 to 7.0.0 (#3876)
|
||||
- feat: support tar archives for installs (#3874)
|
||||
- fix: eliminate TOCTOU race in file unlink calls (#3819)
|
||||
- fix(scripts): tolerate an unusable integration.json in the Python helper (#3785)
|
||||
- fix(catalogs): validate the port in the shared catalog-URL validator, like its mirrors do (#3804)
|
||||
- feat(presets): add opt-in constitution-sync preset (#3873)
|
||||
- fix: reject non-object workflow caches (#3860)
|
||||
- Harden extension URL download cache against symlink and junction races (#3869)
|
||||
- fix: escape workflow step metadata (#3863)
|
||||
- [bug-fix] Fix bundle-update-force-mislead: add refresh() to DefaultPrimitiveInstaller (#3452)
|
||||
- fix: use chunked read for extension manifest hash (#3841)
|
||||
- fix: preserve unreadable event config files (#3861)
|
||||
- fix(scripts): use a .NET Framework-safe trim in the PowerShell init-dir resolver (#3872)
|
||||
- Add ContextForge MCP extension to community catalog (#3487)
|
||||
- fix: normalize non-UTF-8 integration manifests (#3862)
|
||||
- feat: bind gate verdict to workflow input via verdict_input (#3725)
|
||||
- docs: use absolute image URLs in README for PyPI rendering (#3867)
|
||||
- chore: release 0.15.0, begin 0.15.1.dev0 development (#3871)
|
||||
|
||||
## [0.15.0] - 2026-07-30
|
||||
|
||||
### Changed
|
||||
|
||||
- Add yolo to community workflow catalog (#3864)
|
||||
- fix(workflows): guard the shell step's timeout check against OverflowError (#3865)
|
||||
- Add Intent Reconciliation extension to community catalog (#3858)
|
||||
- fix(workflows): validate prompt step 'timeout' like the shell step (#3847)
|
||||
- fix: add utf-8 encoding to registry file open calls (#3816)
|
||||
- fix: eliminate TOCTOU race in file unlink calls (#3815)
|
||||
- test(workflows): name the condition-rejection tests for the real boundary (#3808)
|
||||
- fix: eliminate TOCTOU race in file unlink calls (#3811)
|
||||
- fix(presets): escape user-supplied catalog name/URL in add/remove output (#3806)
|
||||
- fix: add missing utf-8 encoding to registry file open calls (#3810)
|
||||
- [bug-fix] Fix upgrade-overwrites-copilot-skills: pass force=True to extension skill re-registration after upgrade (#3853)
|
||||
- fix(integrations): don't abort uninstall when the manifest can't be deleted (#3805)
|
||||
- test(extensions): update stale manifest validation message assertion (#3859)
|
||||
- fix(agents): coerce a non-string description in TOML command rendering (#3799)
|
||||
- fix(workflows): make security requirements sync deterministic (#3832)
|
||||
- fix(cli): render the literal [suffix] in --tag help and rejection message (#3800)
|
||||
- fix(integrations): preserve non-UTF-8 VS Code settings (#3833)
|
||||
- fix(bundler): treat an explicit-null manifest field as missing, not the text "None" (#3798)
|
||||
- feat: first-class agent-native runtime hooks for integrations (#3704)
|
||||
- fix(extensions): guard the required manifest sections so one bad extension cannot break `extension list` (#3797)
|
||||
- fix(presets): escape installed preset metadata in Rich output (#3826)
|
||||
- fix(workflows): dispatch prompt steps via the resolved executable (#3793)
|
||||
- chore: release 0.14.4, begin 0.14.5.dev0 development (#3850)
|
||||
|
||||
## [0.14.4] - 2026-07-29
|
||||
|
||||
### Changed
|
||||
|
||||
- fix(bundler): degrade non-UTF-8 config reads into BundlerError (#3784)
|
||||
- fix(workflows): escape the step-progress line so step ids render (and `/` stops failing the run) (#3783)
|
||||
- Update Agent Parity Governance preset to v0.4.1 (#3830)
|
||||
- fix(integrations): reject empty --commands-dir in generic raw_options (#3714)
|
||||
- fix(presets): guard non-list/non-mapping provides.templates in PresetManifest (#3712)
|
||||
- fix(auth): resolve az via shutil.which so azure-cli token works on Windows (#3709)
|
||||
- fix(workflows): reject falsy non-mapping workflow-catalogs.yml top level (#3707)
|
||||
- fix(integrations): render hyphenated /speckit-<name> for Droid (always-slash agent) (#3688)
|
||||
- [preset] Update A11Y Governance preset to v0.4.2 (#3828)
|
||||
- [preset] Update Parallel Autonomous Run Governance to v0.2.4 (#3825)
|
||||
- fix: correct Optional type annotation for _resolved_dir parameter (#3801)
|
||||
- fix: add timeout to prompt step subprocess execution (#3768)
|
||||
- fix: handle tags containing / in GitHub release asset URL resolution (#3767)
|
||||
- fix(presets): escape catalog metadata in discovery output (#3773)
|
||||
- Update Autonomous Run Governance preset to v0.3.3 (#3823)
|
||||
- fix: use bounded read for integration catalog HTTP responses (#3763)
|
||||
- docs: add Simplified Chinese translation of README (#3740)
|
||||
- Update Intake Sequencing Governance preset to v0.2.2 (#3809)
|
||||
- fix(workflows): reject non-string/non-boolean 'condition' in if/while/do-while steps (#3706)
|
||||
- fix(bundle): escape catalog metadata in discovery output (#3774)
|
||||
- fix(workflows,extensions): tolerate non-list catalog tags in search/info display (#3770)
|
||||
- fix: correct nullable resolved directory annotation (#3771)
|
||||
- fix(presets): tolerate non-string and non-list catalog fields in preset search/info (#3769)
|
||||
- fix(integrations): escape catalog metadata in discovery output (#3772)
|
||||
- Update Verify Review Ship extension to v0.4.2 (#3792)
|
||||
- fix(integrations): preserve native skill invocation prefixes (#3663)
|
||||
- Update Intake Review Governance preset to v0.2.0 (#3796)
|
||||
- fix(constitution): stop propagating guidance into templates (#3737) (#3790)
|
||||
- chore: release 0.14.3, begin 0.14.4.dev0 development (#3795)
|
||||
|
||||
## [0.14.3] - 2026-07-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Intake Authoring Governance preset to v0.3.0 (#3788)
|
||||
- fix(copilot): honor preset command template overrides (#3592)
|
||||
- clarify: require real interrogatives, ban topic-label questions (#3745)
|
||||
- feat: Add Alquimia AI integration (#2734)
|
||||
- harden: secure extension and preset archive downloads (#3141)
|
||||
- fix: correct Optional type annotation for context_note parameter (#3765)
|
||||
- Update AGENTS.md (#2626)
|
||||
- fix(extensions): tolerate non-string catalog name in display-name lookup (#3747)
|
||||
- fix(presets): coerce non-string catalog tags before joining (#3743)
|
||||
- fix: register extensions for the active integration only (#3459)
|
||||
- fix(extensions): tolerate non-string tags in catalog search (#3746)
|
||||
- fix(extensions): hyphenate command names in 'extension info' listing (#3744)
|
||||
- fix(workflows): escape remaining untrusted fields in `workflow info` (#3731)
|
||||
- fix(extensions): guard non-numeric catalog downloads in search/info rendering (#3710)
|
||||
- fix(agent-context): apply default markers when config markers are blank (bash) (#3736)
|
||||
- fix: escape Rich markup in catalog list output (#3738)
|
||||
- fix(workflows): guard non-mapping 'workflow:' block in WorkflowDefinition (#3694)
|
||||
- fix(bundler): reject unsupported schema_version in _merge_config (align readers) (#3711)
|
||||
- Update Linear Weave extension to v1.0.1 (#3762)
|
||||
- Add Intake Sequencing Governance preset to community catalog (#3761)
|
||||
- Update Quality Gates (Enforcement Layer) extension to v0.3.3 (#3760)
|
||||
- Update Verify Review Ship extension to v0.4.1 (#3759)
|
||||
- fix(agent-context): discover nested plans in Python port mtime fallback (#3734)
|
||||
- fix(extensions): make shipped scripts executable after install (#3723)
|
||||
- docs(assess): clarify the pipeline works on an empty project (#3732)
|
||||
- chore: release 0.14.2, begin 0.14.3.dev0 development (#3730)
|
||||
|
||||
## [0.14.2] - 2026-07-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Intake Review Governance preset to v0.1.1 (#3729)
|
||||
- Update Verify Review Ship extension to v0.3.0 (#3728)
|
||||
- Update Architecture Guard extension to v1.13.1 (#3724)
|
||||
- docs(upgrade): Claude Code files live in .claude/skills, not .claude/commands (#3708)
|
||||
- fix(kilocode): install commands under .kilo/commands (#3672)
|
||||
- fix(auth): normalize whitespace in auth-config env-var/id references at store time (#3691)
|
||||
- fix(workflows): guard non-mapping 'inputs:' block in engine._resolve_inputs (#3696)
|
||||
- Update Intake Authoring Governance preset to v0.2.0 (#3721)
|
||||
- docs: clarify shell-step interpolation safety (#3719)
|
||||
- [extension] Add Blueprint Index — Living Architecture Map extension to community catalog (#3718)
|
||||
- fix(github-http): return None on malformed host in resolve_github_release_asset_api_url (#3715)
|
||||
- fix(integrations): declare PiIntegration multi_install_safe (#3652)
|
||||
- harden: remove shell parameter from run_command() (#3716)
|
||||
- chore(deps): bump github/codeql-action/init from 4.37.1 to 4.37.3 (#3699)
|
||||
- fix: auto-correct conflicting feature prefixes (#1829)
|
||||
- chore(deps): bump actions/checkout from 6.0.3 to 7.0.1 (#3703)
|
||||
- chore(deps): bump DavidAnson/markdownlint-cli2-action (#3702)
|
||||
- chore(deps): bump actions/setup-node from 6.4.0 to 7.0.0 (#3701)
|
||||
- chore(deps): bump astral-sh/setup-uv from 8.3.2 to 9.0.0 (#3700)
|
||||
- chore: release 0.14.1, begin 0.14.2.dev0 development (#3698)
|
||||
|
||||
## [0.14.1] - 2026-07-23
|
||||
|
||||
### Changed
|
||||
|
||||
- Update Agent Parity Governance preset to v0.4.0 (#3697)
|
||||
- fix(bundler): InstallResult.changed counts uninstalled as a change (#3692)
|
||||
- [preset] Update Cross-Platform Governance preset to v0.2.1 (#3695)
|
||||
- Update A11Y Governance preset to v0.4.1 (#3693)
|
||||
- fix(workflows): escape step-graph brackets in `workflow info` so the type shows (#3690)
|
||||
- fix(workflows): filter parser rejects trailing tokens (fullmatch, not match) (#3689)
|
||||
- Update iSAQB Architecture Governance preset to v0.2.1 (#3687)
|
||||
- fix(extensions): parse SKILL.md on the --- delimiter line during removal (#3634)
|
||||
- fix(cli): guard lazy .hostname ValueError in extension/preset add --from (#3651)
|
||||
- Update Architecture Governance preset to v0.5.1 (#3686)
|
||||
- fix(bundler): reject a top-level non-mapping bundle-catalogs.yml in _merge_config (#3659)
|
||||
- Update Security Governance preset to v0.6.1 (#3685)
|
||||
- fix(integrations): declare OmpIntegration multi_install_safe (#3650)
|
||||
- feat(git-extension): add configurable Conventional Commit support (#3390) (#3413)
|
||||
- fix(extensions): hyphenate command names in the Forge post-install listing (#3669)
|
||||
- fix(bundler): reject falsy non-mapping requires/provides in CatalogEntry.from_dict (#3667)
|
||||
- fix(bundler): reject falsy non-list bundles/contributed_components in records (#3666)
|
||||
- Update Intake Authoring Governance preset to v0.1.1 (#3678)
|
||||
- docs(extensions): clarify agent-context README and add config examples (#3389)
|
||||
- chore: release 0.14.0, begin 0.14.1.dev0 development (#3677)
|
||||
|
||||
## [0.14.0] - 2026-07-23
|
||||
|
||||
### Changed
|
||||
|
||||
- docs: add spec-kit-copilot to community friends (#3675)
|
||||
- fix(integrations): recompute invoke_separator from retained parsed_options (#3664)
|
||||
- fix(workflows): preserve intra-overlay order for multiple insert_after edits (#3662)
|
||||
- fix(bundler): reject falsy non-mapping requires/provides in manifest from_dict (#3661)
|
||||
- fix(bundler): dump_yaml writes literal UTF-8 (allow_unicode=True) (#3660)
|
||||
- fix(integrations): declare kiro-cli multi-install safe (#3477)
|
||||
- fix(git-extension): trim trailing whitespace before stripping commit-message quotes (#3673)
|
||||
- fix(bundler): order bundle members by canonical POSIX arcname (reproducible builds) (#3658)
|
||||
- fix(integrations): Cline overrides post_process_command_content (correct hook name) (#3657)
|
||||
- docs(workflows): gate step docstring lists the 'retry' on_reject behaviour (#3656)
|
||||
- fix: harden bounded reads and redirect validation (#3671)
|
||||
- fix(packaging): bundle scripts/python into the wheel core_pack (#3665) (#3670)
|
||||
- fix: bundle scripts/python in wheel so --script py works (#3665) (#3668)
|
||||
- docs(workflows): init step docstring lists the 'py' script type (#3655)
|
||||
- fix(integrations): declare LingmaIntegration multi_install_safe (#3654)
|
||||
- fix: guard constitution command against feature execution (#3646)
|
||||
- Fix duplicate step numbering in specify command (#3647)
|
||||
- docs(scripts): document the 'py' script type and sh/ps migration plan (#3284) (#3653)
|
||||
- harden: bound HTTP reads and enforce strict redirects (#3140)
|
||||
- chore: release 0.13.4, begin 0.13.5.dev0 development (#3649)
|
||||
|
||||
## [0.13.4] - 2026-07-22
|
||||
|
||||
### Changed
|
||||
|
||||
- docs(concepts): document the spec-of-specs feature breakdown approach (#3648)
|
||||
- fix(scripts): git-ext PowerShell emits the '# To persist' SPECIFY_FEATURE hint (parity) (#3632)
|
||||
- fix(integrations): validate cached catalog shape before returning it (#3627)
|
||||
- fix(bundler): reject non-list 'catalogs' in bundle-catalogs.yml with a clean error (#3623)
|
||||
- fix(bundler): guard lazy .hostname ValueError in catalog add_source (#3644)
|
||||
- Add Intake Authoring Governance preset to community catalog (#3643)
|
||||
- feat: add Factory Droid CLI integration (#822) (#3587)
|
||||
- docs(installation): document the 'py' (Python) script type (#3640)
|
||||
- fix(init): show hyphenated /speckit-<name> in Next Steps for Forge projects (#3642)
|
||||
- fix(extensions): render hyphenated hook invocations for Forge projects (#3641)
|
||||
- fix(workflows): workflow add detects local YAML files case-insensitively (#3633)
|
||||
- fix(workflows): list-literal expression ignores trailing/empty commas (#3631)
|
||||
- fix(workflows): StepRegistry.add tolerates a corrupted non-dict existing entry (#3630)
|
||||
- fix(bundler): reject non-mapping 'integration' in a bundle manifest (#3629)
|
||||
- fix(workflows): command/prompt steps fail cleanly on a non-string integration (#3626)
|
||||
- docs(core): document the 'py' (Python) --script type in the init option table (#3625)
|
||||
- fix(workflows): gate prompt uses isdecimal() so a superscript digit doesn't crash (#3624)
|
||||
- fix(integrations): Cline dispatches hyphenated /speckit-<cmd> invocations (#3622)
|
||||
- docs(upgrade): document integration upgrade / extension update as the project-files upgrade path (#3326)
|
||||
- chore: release 0.13.3, begin 0.13.4.dev0 development (#3645)
|
||||
|
||||
## [0.13.3] - 2026-07-22
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/github/spec-kit/main/media/logo_large.webp" alt="Spec Kit Logo" width="200" height="200"/>
|
||||
<img src="./media/logo_large.webp" alt="Spec Kit Logo" width="200" height="200"/>
|
||||
<h1>🌱 Spec Kit</h1>
|
||||
<h3><em>Define what to build before building it — with any AI coding agent.</em></h3>
|
||||
</div>
|
||||
@@ -15,11 +15,6 @@
|
||||
<a href="https://github.github.io/spec-kit/"><img src="https://img.shields.io/badge/docs-GitHub_Pages-blue" alt="Documentation"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>English</strong> ·
|
||||
<a href="./README.zh-CN.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
@@ -136,7 +131,7 @@ For detailed step-by-step instructions, see our [comprehensive guide](./spec-dri
|
||||
|
||||
Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)!
|
||||
|
||||
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
||||
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
||||
|
||||
## 🌍 Community
|
||||
|
||||
|
||||
361
README.zh-CN.md
361
README.zh-CN.md
@@ -1,361 +0,0 @@
|
||||
<div align="center">
|
||||
<img src="./media/logo_large.webp" alt="Spec Kit Logo" width="200" height="200"/>
|
||||
<h1>🌱 Spec Kit</h1>
|
||||
<h3><em>在动手编码之前,先定义要构建什么 —— 适配任意 AI 编码助手。</em></h3>
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<strong>一个开源工具套件,帮助你借助任意 AI 编码助手构建高质量软件 —— 内置开箱即用的规范驱动流程(也可自带流程),可无限扩展、由社区驱动,并为整个组织的协作而设计。</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/github/spec-kit/releases/latest"><img src="https://img.shields.io/github/v/release/github/spec-kit" alt="Latest Release"/></a>
|
||||
<a href="https://github.com/github/spec-kit/stargazers"><img src="https://img.shields.io/github/stars/github/spec-kit?style=social" alt="GitHub stars"/></a>
|
||||
<a href="https://github.com/github/spec-kit/blob/main/LICENSE"><img src="https://img.shields.io/github/license/github/spec-kit" alt="License"/></a>
|
||||
<a href="https://github.github.io/spec-kit/"><img src="https://img.shields.io/badge/docs-GitHub_Pages-blue" alt="Documentation"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="./README.md">English</a> ·
|
||||
<strong>简体中文</strong>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [🤔 什么是规范驱动开发?](#-什么是规范驱动开发)
|
||||
- [⚡ 快速开始](#-快速开始)
|
||||
- [📽️ 视频概览](#️-视频概览)
|
||||
- [🌍 社区](#-社区)
|
||||
- [🤖 支持的 AI 编码助手集成](#-支持的-ai-编码助手集成)
|
||||
- [🔧 Specify CLI 参考](#-specify-cli-参考)
|
||||
- [🧩 打造你自己的 Spec Kit:扩展与预设](#-打造你自己的-spec-kit扩展与预设)
|
||||
- [📦 捆绑包:面向角色的一键配置](#-捆绑包面向角色的一键配置)
|
||||
- [📚 核心理念](#-核心理念)
|
||||
- [🌟 开发阶段](#-开发阶段)
|
||||
- [🎯 实验目标](#-实验目标)
|
||||
- [🔧 环境要求](#-环境要求)
|
||||
- [📖 深入了解](#-深入了解)
|
||||
- [💬 支持](#-支持)
|
||||
- [🙏 致谢](#-致谢)
|
||||
- [📄 许可证](#-许可证)
|
||||
|
||||
## 🤔 什么是规范驱动开发?
|
||||
|
||||
规范驱动开发(Spec-Driven Development)**颠覆了**传统软件开发的思路。几十年来,代码一直是核心 —— 规范只是编码这项"正事"开始前搭起、随后就被丢弃的脚手架。规范驱动开发改变了这一点:**规范本身变得可执行**,它不再只是引导实现,而是直接生成可运行的实现。
|
||||
|
||||
## ⚡ 快速开始
|
||||
|
||||
### 1. 安装 Specify CLI
|
||||
|
||||
需要 **[uv](https://docs.astral.sh/uv/)**([安装 uv](./docs/install/uv.md))。将 `vX.Y.Z` 替换为 [Releases](https://github.com/github/spec-kit/releases) 中最新的发布标签 —— 记得保留开头的 `v`(例如 `v0.12.11`,而不是 `0.12.11`):
|
||||
|
||||
```bash
|
||||
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@vX.Y.Z
|
||||
```
|
||||
|
||||
更倾向从 PyPI 安装?`specify-cli` 包同样发布在那里:
|
||||
|
||||
```bash
|
||||
uv tool install specify-cli
|
||||
```
|
||||
|
||||
其他安装方式、安装校验、升级以及故障排查,请参阅[安装指南](./docs/installation.md)。
|
||||
|
||||
### 2. 初始化项目
|
||||
|
||||
```bash
|
||||
specify init my-project --integration copilot
|
||||
cd my-project
|
||||
```
|
||||
|
||||
要检查更新或升级已安装的 CLI,可使用自管理命令。更详细的场景和自定义选项请参阅[升级指南](./docs/upgrade.md)。
|
||||
|
||||
```bash
|
||||
# 检查是否有更新版本可用(只读操作 —— 不会修改任何内容)
|
||||
specify self check
|
||||
|
||||
# 预览升级将执行的操作,但不实际升级
|
||||
specify self upgrade --dry-run
|
||||
|
||||
# 就地升级到最新稳定版(自动识别 uv tool 与 pipx 安装方式)
|
||||
specify self upgrade
|
||||
|
||||
# 或锁定到指定的发布标签(将 vX.Y.Z[suffix] 替换为你想要的标签)
|
||||
specify self upgrade --tag vX.Y.Z[suffix]
|
||||
```
|
||||
|
||||
直接运行 `specify self upgrade` 会立即执行,与 `pip install -U`、`npm update` 等命令一样无需额外确认。对于 `uv tool` 安装的情况,它在底层会执行 `uv tool install specify-cli --force --from <git ref>`,因此锁定的发布标签同样有效,包括 dev、alpha/beta/rc 或带构建元数据的后缀。`uvx`(临时运行)和源码检出会被自动识别,此时会给出针对具体路径的操作建议,而不会执行安装程序。可通过设置 `SPECIFY_UPGRADE_TIMEOUT_SECS` 来限制安装子进程的最长运行时间(默认无超时限制 —— 必要时用 `Ctrl+C` 中断)。
|
||||
|
||||
### 3. 确立项目准则
|
||||
|
||||
在项目目录下启动你的编码助手。大多数助手将 spec-kit 暴露为 `/speckit.*` 斜杠命令;处于技能(skills)模式的 Codex CLI 则使用 `$speckit-*`;GitHub Copilot CLI 使用 `/agents` 来选择助手,或直接在提示词中指定它。
|
||||
|
||||
使用 **`/speckit.constitution`** 命令来创建项目的治理准则和开发指南,它们将指导后续所有开发工作。
|
||||
|
||||
```bash
|
||||
/speckit.constitution Create principles focused on code quality, testing standards, user experience consistency, and performance requirements
|
||||
```
|
||||
|
||||
### 4. 编写规范
|
||||
|
||||
使用 **`/speckit.specify`** 命令描述你想构建什么。聚焦于**做什么**和**为什么做**,而不是技术栈。
|
||||
|
||||
```bash
|
||||
/speckit.specify Build an application that can help me organize my photos in separate photo albums. Albums are grouped by date and can be re-organized by dragging and dropping on the main page. Albums are never in other nested albums. Within each album, photos are previewed in a tile-like interface.
|
||||
```
|
||||
|
||||
### 5. 制定技术实现方案
|
||||
|
||||
使用 **`/speckit.plan`** 命令提供你的技术栈和架构选择。
|
||||
|
||||
```bash
|
||||
/speckit.plan The application uses Vite with minimal number of libraries. Use vanilla HTML, CSS, and JavaScript as much as possible. Images are not uploaded anywhere and metadata is stored in a local SQLite database.
|
||||
```
|
||||
|
||||
### 6. 拆解为任务
|
||||
|
||||
使用 **`/speckit.tasks`** 从实现方案生成一份可执行的任务清单。
|
||||
|
||||
```bash
|
||||
/speckit.tasks
|
||||
```
|
||||
|
||||
### 7. 执行实现
|
||||
|
||||
使用 **`/speckit.implement`** 执行所有任务,按方案构建你的功能。
|
||||
|
||||
```bash
|
||||
/speckit.implement
|
||||
```
|
||||
|
||||
详细的分步说明,请参阅我们的[完整指南](./spec-driven.md)。
|
||||
|
||||
## 📽️ 视频概览
|
||||
|
||||
想看看 Spec Kit 的实际效果?观看我们的[视频概览](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)!
|
||||
|
||||
[](https://www.youtube.com/watch?v=a9eR1xsfvHg&pp=0gcJCckJAYcqIYzv)
|
||||
|
||||
## 🌍 社区
|
||||
|
||||
在 [Spec Kit 文档站点](https://github.github.io/spec-kit/)上探索由社区贡献的资源:
|
||||
|
||||
- [扩展(Extensions)](https://github.github.io/spec-kit/community/extensions.html) —— 命令、钩子与各类能力
|
||||
- [预设(Presets)](https://github.github.io/spec-kit/community/presets.html) —— 模板与术语覆盖
|
||||
- [捆绑包(Bundles)](https://github.github.io/spec-kit/community/bundles.html) —— 由现有组件组合而成的角色与团队技术栈
|
||||
- [实战演练(Walkthroughs)](https://github.github.io/spec-kit/community/walkthroughs.html) —— 端到端的 SDD 场景
|
||||
- [伙伴项目(Friends)](https://github.github.io/spec-kit/community/friends.html) —— 扩展 Spec Kit 或基于它构建的项目
|
||||
|
||||
> [!NOTE]
|
||||
> 社区贡献由各自的作者独立创建和维护。请在安装前审阅源代码,并自行斟酌使用。
|
||||
|
||||
想要参与贡献?请参阅[扩展发布指南](extensions/EXTENSION-PUBLISHING-GUIDE.md)、[预设发布指南](presets/PUBLISHING.md)或[社区捆绑包指南](docs/community/bundles.md)。
|
||||
|
||||
## 🤖 支持的 AI 编码助手集成
|
||||
|
||||
Spec Kit 可与 30 多个 AI 编码助手协作 —— 既包括 CLI 工具,也包括基于 IDE 的助手。完整列表以及相关说明和使用细节,请参阅[支持的 AI 编码助手集成](https://github.github.io/spec-kit/reference/integrations.html)指南。
|
||||
|
||||
运行 `specify integration list` 可查看当前安装版本中所有可用的集成。
|
||||
|
||||
## 可用的斜杠命令
|
||||
|
||||
运行 `specify init` 后,你的 AI 编码助手就能使用这些斜杠命令来进行结构化开发。对于支持技能模式的集成,传入 `--integration <agent> --integration-options="--skills"` 会安装助手技能,而不是斜杠命令的提示词文件。
|
||||
|
||||
### 核心命令
|
||||
|
||||
规范驱动开发工作流中必不可少的命令:
|
||||
|
||||
| 命令 | 助手技能 | 说明 |
|
||||
| ------------------------ | ---------------------- | ---------------------------------------------------------- |
|
||||
| `/speckit.constitution` | `speckit-constitution` | 创建或更新项目的治理准则和开发指南 |
|
||||
| `/speckit.specify` | `speckit-specify` | 定义你想构建什么(需求与用户故事) |
|
||||
| `/speckit.plan` | `speckit-plan` | 结合所选技术栈制定技术实现方案 |
|
||||
| `/speckit.tasks` | `speckit-tasks` | 生成可执行的实现任务清单 |
|
||||
| `/speckit.taskstoissues` | `speckit-taskstoissues`| 将生成的任务清单转换为 GitHub issue,便于跟踪与执行 |
|
||||
| `/speckit.implement` | `speckit-implement` | 执行所有任务,按方案构建功能 |
|
||||
| `/speckit.converge` | `speckit-converge` | 对照规范/方案/任务评估代码库,并将剩余工作追加为新任务 |
|
||||
|
||||
### 可选命令
|
||||
|
||||
用于提升质量与做校验的额外命令:
|
||||
|
||||
| 命令 | 助手技能 | 说明 |
|
||||
| -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `/speckit.clarify` | `speckit-clarify` | 澄清描述不充分的部分(建议在 `/speckit.plan` 之前使用;旧称 `/quizme`) |
|
||||
| `/speckit.analyze` | `speckit-analyze` | 跨制品的一致性与覆盖度分析(在 `/speckit.tasks` 之后、`/speckit.implement` 之前运行) |
|
||||
| `/speckit.checklist` | `speckit-checklist` | 生成自定义质量清单,校验需求的完整性、清晰度与一致性(好比"为自然语言写单元测试") |
|
||||
|
||||
## 🔧 Specify CLI 参考
|
||||
|
||||
完整的命令详情、选项与示例,请参阅 [CLI 参考文档](https://github.github.io/spec-kit/reference/overview.html)。
|
||||
|
||||
## 🧩 打造你自己的 Spec Kit:扩展与预设
|
||||
|
||||
Spec Kit 可通过两套互补的机制进行深度定制 —— **扩展(extensions)** 和 **预设(presets)** —— 以及面向单个项目的本地覆盖,用于临时性调整:
|
||||
|
||||
| 优先级 | 组件类型 | 位置 |
|
||||
| -----: | ---------------------------------- | -------------------------------- |
|
||||
| ⬆ 1 | 项目本地覆盖 | `.specify/templates/overrides/` |
|
||||
| 2 | 预设 —— 定制核心与扩展 | `.specify/presets/templates/` |
|
||||
| 3 | 扩展 —— 新增能力 | `.specify/extensions/templates/` |
|
||||
| ⬇ 4 | Spec Kit 核心 —— 内置 SDD 命令与模板 | `.specify/templates/` |
|
||||
|
||||
- **模板**在**运行时**解析 —— Spec Kit 从高到低遍历优先级栈,使用第一个匹配项。
|
||||
- 项目本地覆盖(`.specify/templates/overrides/`)允许对单个项目做一次性调整,无需创建完整的预设。
|
||||
- **扩展/预设命令**在**安装时**生效 —— 当你运行 `specify extension add` 或 `specify preset add` 时,命令文件会被写入助手目录(如 `.claude/commands/`)。
|
||||
- 若多个预设或扩展提供了同一命令,优先级最高的版本生效。移除时,次优先级的版本会自动恢复。
|
||||
- 若不存在任何覆盖或自定义,Spec Kit 使用核心默认配置。
|
||||
|
||||
### 扩展 —— 新增能力
|
||||
|
||||
当你需要 Spec Kit 核心之外的功能时,使用**扩展**。扩展可引入新命令和模板 —— 例如添加核心 SDD 命令未覆盖的领域特定工作流、集成外部工具,或新增全新的开发阶段。它们扩展了 *Spec Kit 能做什么*。
|
||||
|
||||
```bash
|
||||
# 搜索可用扩展
|
||||
specify extension search
|
||||
|
||||
# 安装扩展
|
||||
specify extension add <extension-name>
|
||||
```
|
||||
|
||||
举例来说,扩展可以添加 Jira 集成、实现后代码审查、V 模型测试追溯性,或项目健康诊断等功能。
|
||||
|
||||
完整命令指南请参阅[扩展参考文档](https://github.github.io/spec-kit/reference/extensions.html)。浏览[社区扩展](https://github.github.io/spec-kit/community/extensions.html)了解现有资源。
|
||||
|
||||
### 预设 —— 定制现有工作流
|
||||
|
||||
当你想改变 Spec Kit 的*工作方式*而不是新增能力时,使用**预设**。预设会覆盖核心及已安装扩展中附带的模板和命令 —— 例如强制使用面向合规的规范格式、采用领域特定术语,或对方案和任务应用组织规范。预设定制的是 Spec Kit 及其扩展生成的制品与指令。
|
||||
|
||||
```bash
|
||||
# 搜索可用预设
|
||||
specify preset search
|
||||
|
||||
# 安装预设
|
||||
specify preset add <preset-name>
|
||||
```
|
||||
|
||||
举例来说,预设可以重构规范模板以要求监管追溯性,将工作流适配为你所用的方法论(如敏捷、看板、瀑布、用户任务驱动或领域驱动设计),在方案中添加强制安全审查关卡,强制要求测试优先的任务排序,或将整个工作流本地化为其他语言。[海盗语演示](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)充分展示了定制的深度。多个预设可按优先级叠加使用。
|
||||
|
||||
完整命令指南以及解析顺序和优先级叠加说明,请参阅[预设参考文档](https://github.github.io/spec-kit/reference/presets.html)。
|
||||
|
||||
## 📦 捆绑包:面向角色的一键配置
|
||||
|
||||
扩展和预设是独立的构建模块。而**捆绑包(bundle)**将一组精选的扩展、预设、步骤和工作流打包成一个带版本、面向角色的配置,从而可以用一条命令为整个团队角色(产品经理、业务分析师、安全研究员、开发者……)完成配置。
|
||||
|
||||
捆绑包由一份手写的 `bundle.yml` 清单描述。它将每个组件锁定到具体版本,并可选择性地面向特定集成;未指定 `integration` 的捆绑包是**中立的**,会沿用项目当前已使用的集成。
|
||||
|
||||
```bash
|
||||
# 在当前激活的目录栈中发现捆绑包
|
||||
specify bundle search [<query>]
|
||||
|
||||
# 查看捆绑包将添加的确切组件集合(与实际安装的内容一致)
|
||||
specify bundle info <bundle-id>
|
||||
|
||||
# 一步安装捆绑包的完整组件集合
|
||||
specify bundle install <bundle-id>
|
||||
|
||||
# 查看已安装内容,然后以非破坏性方式更新或移除
|
||||
specify bundle list
|
||||
specify bundle update <bundle-id> # 或 --all
|
||||
specify bundle remove <bundle-id> # 仅移除此捆绑包的组件
|
||||
```
|
||||
|
||||
捆绑包从一个**按优先级排序的目录栈**(项目 > 用户 > 内置)中解析。每个来源都带有安装策略:`install-allowed` 来源可用于安装,而 `discovery-only` 来源在 `search`/`info` 中可见但拒绝安装。可通过 `specify bundle catalog list|add|remove` 管理目录栈。
|
||||
|
||||
作者在本地校验并打包捆绑包。分发方式是托管构建产物并添加一个目录来源;社区捆绑包投稿请使用 [Bundle Submission](https://github.com/github/spec-kit/issues/new?template=bundle_submission.yml) issue 模板,以便对所需的组件目录和安装证据进行审阅:
|
||||
|
||||
```bash
|
||||
specify bundle validate --path ./my-bundle # 结构与引用检查
|
||||
specify bundle build --path ./my-bundle # 生成带版本的 .zip 产物
|
||||
```
|
||||
|
||||
[`examples/bundles/`](examples/bundles/) 目录下有四份可直接阅读的示例清单(产品经理、业务分析师、安全研究员、开发者)。
|
||||
|
||||
关键保证:`info` 展示的内容与 `install` 添加的内容完全一致(透明性);安装是幂等的,且限定在项目根目录内;`remove` 绝不会触碰其他已安装捆绑包仍需要的组件;所有消费/创作命令都能针对本地或锁定的来源**离线**工作。
|
||||
|
||||
### 何时用哪个
|
||||
|
||||
| 目标 | 使用 |
|
||||
| --- | --- |
|
||||
| 添加全新的命令或工作流 | 扩展 |
|
||||
| 定制规范、方案或任务的格式 | 预设 |
|
||||
| 集成外部工具或服务 | 扩展 |
|
||||
| 强制执行组织或监管规范 | 预设 |
|
||||
| 交付可复用的领域特定模板 | 均可 —— 预设用于模板覆盖,扩展用于随新命令一起打包的模板 |
|
||||
| 用一条命令完成完整的角色配置 | 捆绑包 |
|
||||
|
||||
## 📚 核心理念
|
||||
|
||||
规范驱动开发是一套结构化流程,它强调:
|
||||
|
||||
- **意图驱动开发** —— 让规范先定义"*做什么*",再谈"*怎么做*"
|
||||
- **丰富的规范撰写** —— 借助护栏与组织准则来编写规范
|
||||
- **多步精炼** —— 而非从提示词一次性生成代码
|
||||
- **充分依赖**先进 AI 模型对规范的解读能力
|
||||
|
||||
## 🌟 开发阶段
|
||||
|
||||
| 阶段 | 侧重点 | 关键活动 |
|
||||
| ----------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **从 0 到 1 开发**("绿地/Greenfield") | 从零生成 | <ul><li>从高层需求出发</li><li>生成规范</li><li>规划实现步骤</li><li>构建生产就绪的应用</li></ul> |
|
||||
| **创意探索** | 并行实现 | <ul><li>探索多样化的解决方案</li><li>支持多种技术栈与架构</li><li>试验不同的用户体验模式</li></ul> |
|
||||
| **迭代增强**("棕地/Brownfield") | 存量系统现代化 | <ul><li>迭代式添加功能</li><li>现代化改造遗留系统</li><li>调整流程</li></ul> |
|
||||
|
||||
对于已有项目,请将 Spec Kit 工具本身的更新与功能制品的演进分开处理:升级时刷新受管理的项目文件,而在预期行为发生变化时更新 `specs/` 制品。[规范演进指南](./docs/guides/evolving-specs.md)介绍了推荐的棕地迭代循环。
|
||||
|
||||
## 🎯 实验目标
|
||||
|
||||
我们的研究与实验聚焦于:
|
||||
|
||||
### 技术无关性
|
||||
|
||||
- 使用多样化的技术栈构建应用
|
||||
- 验证这一假设:规范驱动开发是一套流程,不与特定技术、编程语言或框架绑定
|
||||
|
||||
### 企业级约束
|
||||
|
||||
- 展示关键业务应用的开发
|
||||
- 纳入组织层面的约束(云服务商、技术栈、工程实践)
|
||||
- 支持企业设计系统与合规要求
|
||||
|
||||
### 以用户为中心的开发
|
||||
|
||||
- 为不同的用户群体和偏好构建应用
|
||||
- 支持多种开发方式(从"氛围编码"到 AI 原生开发)
|
||||
|
||||
### 创意与迭代流程
|
||||
|
||||
- 验证并行实现探索的理念
|
||||
- 提供稳健的迭代式功能开发工作流
|
||||
- 将流程扩展到升级与现代化改造任务
|
||||
|
||||
## 🔧 环境要求
|
||||
|
||||
- **Linux/macOS/Windows**
|
||||
- [受支持的](#-支持的-ai-编码助手集成) AI 编码助手。
|
||||
- [uv](https://docs.astral.sh/uv/) 用于包管理(推荐),或 [pipx](https://pipx.pypa.io/) 用于持久化安装
|
||||
- [Python 3.11+](https://www.python.org/downloads/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
如果你在使用某个助手时遇到问题,欢迎提交 issue,以便我们完善相应集成。
|
||||
|
||||
## 📖 深入了解
|
||||
|
||||
- **[完整的规范驱动开发方法论](./spec-driven.md)** —— 深入了解整个流程
|
||||
- **[快速上手指南](https://github.github.io/spec-kit/quickstart.html)** —— 分步实现演练
|
||||
|
||||
---
|
||||
|
||||
## 💬 支持
|
||||
|
||||
如需帮助,请提交 [GitHub issue](https://github.com/github/spec-kit/issues/new)。我们欢迎缺陷报告、功能建议,以及关于使用规范驱动开发的各类问题。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
本项目深受 [John Lam](https://github.com/jflam) 的工作与研究的影响,并在其基础上构建。
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目基于 MIT 开源许可证的条款授权。完整条款请参阅 [LICENSE](./LICENSE) 文件。
|
||||
@@ -36,7 +36,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| 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) |
|
||||
| Blueprint Index — Living Architecture Map | A living architecture map for spec-driven projects, kept honest by a deterministic, low-friction, machine-first CI gate (JSON, self-healable) that blocks only when the map contradicts the specs or code. Brownfield or greenfield. | `process` | Read+Write | [spec-kit-blueprint](https://github.com/ogil109/spec-kit-blueprint) |
|
||||
| Branch Convention | Configurable branch and folder naming conventions for /specify with presets and custom patterns | `process` | Read+Write | [spec-kit-branch-convention](https://github.com/Quratulain-bilal/spec-kit-branch-convention) |
|
||||
| Brownfield Bootstrap | Bootstrap spec-kit for existing codebases — auto-discover architecture and adopt SDD incrementally | `process` | Read+Write | [spec-kit-brownfield](https://github.com/Quratulain-bilal/spec-kit-brownfield) |
|
||||
| BrownKit | Evidence-driven capability discovery, security and QA risk assessment for existing codebases | `process` | Read+Write | [BrownKit](https://github.com/MaksimShevtsov/BrownKit) |
|
||||
@@ -50,7 +49,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Coding Standards Drift Control | Generate coding-standards drift reports and remediation tasks for active Spec Kit features | `code` | Read+Write | [spec-kit-coding-standards-drift-control](https://github.com/benizzio/spec-kit-coding-standards-drift-control) |
|
||||
| Conduct Extension | Orchestrates spec-kit phases via sub-agent delegation to reduce context pollution. | `process` | Read+Write | [spec-kit-conduct-ext](https://github.com/twbrandon7/spec-kit-conduct-ext) |
|
||||
| Confluence Extension | Create a doc in Confluence summarizing the specifications and planning files | `integration` | Read+Write | [spec-kit-confluence](https://github.com/aaronrsun/spec-kit-confluence) |
|
||||
| ContextForge MCP | Integrates codebase-memory-mcp + headroom into Spec Kit — graph-based code intelligence and context compression for the implement phase | `code` | Read+Write | [contextforge-mcp](https://github.com/capatinore/contextforge-mcp) |
|
||||
| Cost Tracker | Track real LLM dollar cost across SDD workflows — per-feature budgets, per-integration comparison, and finance-ready exports | `visibility` | Read+Write | [spec-kit-cost](https://github.com/Quratulain-bilal/spec-kit-cost) |
|
||||
| Data Model Diagram | Generates Mermaid ER diagrams from Spec Kit data models after planning | `docs` | Read+Write | [spec-kit-data-model-diagram](https://github.com/benizzio/spec-kit-data-model-diagram) |
|
||||
| DocGuard — CDD Enforcement | The only doc-integrity engine with an MCP server, SARIF/JUnit output, and a deterministic zero-LLM core. Validates, scores, and traces documentation against code — 27 validators, stable finding codes, adoption baseline for legacy repos, compliance-evidence reports, GitHub Action with PR annotations, spec-kit hooks. Pure Node.js, one pinned dep. | `docs` | Read+Write | [spec-kit-docguard](https://github.com/raccioly/docguard) |
|
||||
@@ -67,7 +65,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| 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) |
|
||||
| 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) |
|
||||
| Intent Reconciliation | Reconcile implementation-discovered decisions against approved feature intent | `process` | Read+Write | [spec-kit-reconcile](https://github.com/SuhaibAslam/spec-kit-reconcile) |
|
||||
| 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) |
|
||||
| Jira Integration (Sync Engine) | Idempotent, drift-aware, fail-closed reconcile engine mirroring spec-kit specs into Jira (Epic per repo, Story per spec, Subtask per phase) | `integration` | Read+Write | [spec-kit-jira-sync](https://github.com/ashbrener/spec-kit-jira-sync) |
|
||||
@@ -161,7 +158,7 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| Token Economy | Token routing, measured savings, and context audit workflows | `process` | Read+Write | [spec-kit-token-economy](https://github.com/formin/spec-kit-token-economy) |
|
||||
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
|
||||
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
|
||||
| Verify Review Ship | Post-convergence operational verification, technical review, learning governance, and transactional delivery. | `process` | Read+Write | [spec-kit-verify-review-ship](https://github.com/cadugevaerd/spec-kit-verify-review-ship) |
|
||||
| Verify Review Ship | Adds post-implementation verify, review, and ship readiness gates to Spec Kit workflows | `process` | Read-only | [spec-kit-verify-review-ship](https://github.com/cadugevaerd/spec-kit-verify-review-ship) |
|
||||
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
|
||||
| Version Guard | Verify tech stack versions against live npm registries before planning and implementation | `process` | Read-only | [spec-kit-version-guard](https://github.com/KevinBrown5280/spec-kit-version-guard) |
|
||||
| What-if Analysis | Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them | `visibility` | Read-only | [spec-kit-whatif](https://github.com/DevAbdullah90/spec-kit-whatif) |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Community Friends
|
||||
|
||||
> [!NOTE]
|
||||
> Community projects listed here are independently created and maintained by their respective authors. Unless explicitly marked as a **first-party GitHub project**, they are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
|
||||
> Community projects listed here are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their source code before installation and use at your own discretion.
|
||||
|
||||
Community projects that extend, visualize, or build on Spec Kit:
|
||||
|
||||
@@ -16,5 +16,3 @@ Community projects that extend, visualize, or build on Spec Kit:
|
||||
- **[cc-spec-kit](https://github.com/speckit-community/cc-spec-kit)** — Community-maintained plugin for Claude Code and GitHub Copilot CLI that installs Spec Kit skills via the plugin marketplace.
|
||||
|
||||
- **[spectatui](https://github.com/tinesoft/spectatui)** — A terminal UI (TUI) dashboard for Spec Kit that lets you track features, manage specifications, integrations, presets, workflows, and extensions, and monitor AI agent workflows. Attach to existing AI sessions or launch new ones from your terminal. Keyboard and mouse support. Light/dark theme support. Customizable and performance-oriented. Requires the `specify` CLI in your PATH.
|
||||
|
||||
- **[spec-kit-copilot](https://github.com/github/spec-kit-copilot)** — _First-party GitHub project._ A GitHub Copilot **skills plugin** that exposes the Spec Kit `specify` CLI to the Copilot agent in both the Copilot CLI and the GitHub Copilot app. It provides a focused skill per `specify` command group — setup, init, check, extensions, presets, bundles, workflows, workflow steps, and self-upgrade — so you can navigate and drive the entire Spec Kit ecosystem through natural language, letting Copilot decide when and how to run the right `specify` commands on your behalf.
|
||||
|
||||
@@ -7,29 +7,27 @@ The following community-contributed presets customize how Spec Kit behaves — o
|
||||
|
||||
| Preset | Purpose | Provides | Requires | URL |
|
||||
|--------|---------|----------|----------|-----|
|
||||
| A11Y Governance | Adds accessibility (WCAG 2.2 AA), accessible text and JSON status parity, bilingual DE/EN delivery, CEFR-B2 readability, inclusive-content governance, didactic inline-code-comment review, and audit-ready Spec-Kit run evidence to Spec Kit | 10 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) |
|
||||
| Agent Parity Governance | Adds shared-guidance and generated-command parity, repository-fleet completion evidence, secret-free runner/status metadata, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across declared AI-agent surfaces. | 6 templates, 3 commands | — | [spec-kit-preset-agent-parity-governance](https://github.com/hindermath/spec-kit-preset-agent-parity-governance) |
|
||||
| A11Y Governance | Adds accessibility (WCAG 2.2 AA), bilingual DE/EN delivery, CEFR-B2 readability, inclusive-content governance, didactic inline-code-comment review, and audit-ready Spec Kit run evidence | 10 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) |
|
||||
| Agent Parity Governance | Adds shared-guidance parity, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across a project's declared AI-agent instruction surfaces so agent guidance does not drift. | 6 templates, 3 commands | — | [spec-kit-preset-agent-parity-governance](https://github.com/hindermath/spec-kit-preset-agent-parity-governance) |
|
||||
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
|
||||
| Architecture Governance | Adds secure software architecture, resumable remote-transaction boundaries, STRIDE+CAPEC threat modeling, arc42 security cross-cutting concepts, S-ADRs, Zero Trust applicability, OWASP SAMM governance, BSI C3A cloud autonomy, BSI C5 cloud compliance assurance, and audit-ready Spec Kit run evidence | 13 templates, 3 commands | — | [spec-kit-preset-architecture-governance](https://github.com/hindermath/spec-kit-preset-architecture-governance) |
|
||||
| Autonomous Run Governance | Adds permission-bounded autonomous delivery, an optional intake-review gate, and preservation of the project's learner and accessibility contract. | 13 templates, 5 commands, 4 scripts | — | [spec-kit-preset-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-autonomous-run-governance) |
|
||||
| Architecture Governance | Adds secure software architecture, STRIDE+CAPEC threat modeling, arc42 security cross-cutting concepts, S-ADRs, Zero Trust applicability, OWASP SAMM governance, BSI C3A cloud autonomy, BSI C5 cloud compliance assurance, and audit-ready Spec Kit run evidence | 13 templates, 3 commands | — | [spec-kit-preset-architecture-governance](https://github.com/hindermath/spec-kit-preset-architecture-governance) |
|
||||
| Autonomous Run Governance | Adds permission-bounded, evidence-first governance for complete autonomous Spec Kit delivery, including validated status, stop, explicit resume, exact-head proof, post-merge closeout, retrospective learning, and an optional policy-driven intake-review gate before feature creation. | 13 templates, 5 commands, 4 scripts | — | [spec-kit-preset-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-autonomous-run-governance) |
|
||||
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
|
||||
| Claude AskUserQuestion | Upgrades `/speckit.clarify` and `/speckit.checklist` on Claude Code from Markdown-table prompts to the native AskUserQuestion picker, with a recommended option and reasoning on every question | 2 commands | — | [spec-kit-preset-claude-ask-questions](https://github.com/0xrafasec/spec-kit-preset-claude-ask-questions) |
|
||||
| Command Density | Compacts the nine core Spec Kit command prompts while preserving scripts, handoffs, placeholders, hook output blocks, and rule structure | 9 commands | — | [spec-kit-preset-command-density](https://github.com/Xopoko/spec-kit-preset-command-density) |
|
||||
| Cross-Platform Governance | Adds Bash/PowerShell and read-only check parity, root-path and native-override review, Unix man pages, bilingual help, Verb-Noun discipline, and audit-ready evidence. | 8 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
|
||||
| Cross-Platform Governance | Adds Bash + PowerShell parity, Unix man-pages, bilingual comment-based help, Verb-Noun Cmdlet discipline, and audit-ready Spec Kit run evidence for scripting projects managed with Spec Kit | 8 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
|
||||
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
|
||||
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose principles. Supports interactive elements like brainstorming, interview, roleplay, and extras like statistics, cover builder, illustration builder, and bio command. Export with templates for KDP, D2D, etc. | 26 templates, 34 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
|
||||
| Game Narrative Writing | Preset for game narrative design and interactive storytelling. It adapts the Spec-Driven Development workflow for game narratives: features become story mechanics, specs become narrative briefs, plans become story maps, and tasks become dialogue and scene-writing tasks. Supports branching narratives, player agency systems, state machines, and interactive dialogue trees. | 37 templates, 34 commands, 5 scripts | — | [speckit-preset-game-narrative-writing](https://github.com/adaumann/speckit-preset-game-narrative-writing) |
|
||||
| Intake Authoring Governance | Governs traceable intake CRUD and language-aware requirements collections with atomic migrations, rollback evidence, and safe series authoring. | 12 templates, 5 commands, 7 scripts | — | [spec-kit-preset-intake-authoring-governance](https://github.com/hindermath/spec-kit-preset-intake-authoring-governance) |
|
||||
| Intake Review Governance | Reviews single, series, campaign, and language-aware requirements collections before Spec Kit execution. | 8 templates, 3 commands, 4 scripts | — | [spec-kit-preset-intake-review-governance](https://github.com/hindermath/spec-kit-preset-intake-review-governance) |
|
||||
| Intake Sequencing Governance | Manages language-aware intake-series order, typed dependencies, lifecycle, and authority-neutral next-candidate selection. | 11 templates, 6 commands, 8 scripts | — | [spec-kit-preset-intake-sequencing-governance](https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance) |
|
||||
| iSAQB Architecture Governance | Adds iSAQB/CPSA-F and arc42 architecture governance with audit-ready evidence for goals, views, resumability, partial-failure scenarios, ADRs, risks, and technical debt. | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
|
||||
| Intake Review Governance | Adds hash-bound review, repair, and status gates for single, series, and campaign intake files before interactive, autonomous, or parallel Spec Kit execution. | 8 templates, 3 commands, 2 scripts | — | [spec-kit-preset-intake-review-governance](https://github.com/hindermath/spec-kit-preset-intake-review-governance) |
|
||||
| iSAQB Architecture Governance | Adds general iSAQB/CPSA-F and arc42 software-architecture governance, including audit-ready Spec Kit run evidence for architecture goals, views, quality scenarios, ADRs, risks, and technical debt. | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
|
||||
| Jira Issue Tracking | Overrides `speckit.taskstoissues` to create Jira epics, stories, and tasks instead of GitHub Issues via Atlassian MCP tools | 1 command | — | [spec-kit-preset-jira](https://github.com/luno/spec-kit-preset-jira) |
|
||||
| Model Driven Engineering | Focuses on streamlined commands, app repository support, cross-spec support, and capability-aware project memory for model-driven engineering workflows | 6 templates, 11 commands | MDE extension | [spec-kit-preset-mde](https://github.com/AI-MDE/spec-kit-preset-mde) |
|
||||
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
|
||||
| Parallel Autonomous Run Governance | Coordinates permission-bounded autonomous campaigns while preserving the project's learner and accessibility contract across workers and consolidation. | 9 templates, 5 commands, 2 scripts | autonomous-run-governance >=0.2.2; optional: intake-review-governance >=0.1.0 | [spec-kit-preset-parallel-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance) |
|
||||
| Parallel Autonomous Run Governance | Coordinates isolated autonomous Spec Kit campaigns with bounded concurrency, mixed agents, resumable consolidation, governed post-merge closeout, schema 1.2, and an optional current intake-review gate before worker scheduling. | 9 templates, 5 commands, 2 scripts | autonomous-run-governance >=0.3.2; optional: intake-review-governance >=0.1.0 | [spec-kit-preset-parallel-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance) |
|
||||
| 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 and secure-coding governance, exact-head and security-gate evidence, provider-failure classification, ASVS, supply-chain transparency, and EU regulatory screening. | 14 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
|
||||
| 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) |
|
||||
| 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) |
|
||||
|
||||
@@ -63,14 +63,10 @@ independently specified sub-features. Each sub-feature gets its own
|
||||
`spec.md`, `plan.md`, and `tasks.md`, and runs through its own
|
||||
specify/plan/tasks/implement cycle.
|
||||
|
||||
This is the "spec of specs" approach: a first pass breaks a massive feature into
|
||||
smaller, self-contained specs that can each be implemented without overwhelming the
|
||||
model. It adds the most overhead, so reserve it for features that are too large to
|
||||
handle any other way.
|
||||
|
||||
See [Spec of Specs](spec-of-specs.md) for the full procedure — how to run the
|
||||
roadmap pass, structure the roadmap artifact, link sub-specs back to it, and a worked
|
||||
example.
|
||||
This is the "spec of specs" approach: the first iteration breaks a massive
|
||||
feature into smaller, self-contained specs that can each be implemented without
|
||||
overwhelming the model. It adds the most overhead, so reserve it for features
|
||||
that are too large to handle any other way.
|
||||
|
||||
## Which Approach to Choose
|
||||
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
# Spec of Specs
|
||||
|
||||
When a feature is too large to run through a single
|
||||
`/speckit.specify` → `/speckit.plan` → `/speckit.tasks` → `/speckit.implement`
|
||||
cycle without the model losing track mid-implementation, you can break it into a
|
||||
**roadmap** of smaller, independently-specified sub-features. This is the "spec of
|
||||
specs" approach: one up-front pass decomposes a massive feature into self-contained
|
||||
specs, and each of those runs through its own specify/plan/tasks/implement cycle.
|
||||
|
||||
> **When to reach for this.** Decomposition adds the most overhead of any strategy
|
||||
> in [Handling Complex Features](complex-features.md). Use it **only when the lighter
|
||||
> options there are insufficient** — first try limiting how many tasks run per
|
||||
> `/speckit.implement` invocation, then sub-agent delegation, then a combination.
|
||||
> Reach for a spec of specs only when even a single phase is too large to handle in
|
||||
> one run.
|
||||
|
||||
The rest of this page describes *how* to do it with the tools you already have. No
|
||||
new commands or extensions are required.
|
||||
|
||||
## The roadmap pass
|
||||
|
||||
Before writing any sub-spec, do a single decomposition pass to produce a roadmap.
|
||||
Treat this as a lightweight planning conversation with your agent, not a full spec:
|
||||
|
||||
1. **State the whole feature.** Describe the large feature (the "epic") in a
|
||||
sentence or two so the agent has the full picture up front.
|
||||
2. **Identify independent slices.** Ask the agent to propose a small set of
|
||||
sub-features that each deliver a coherent piece of the epic and can be specified
|
||||
on their own. Aim for slices that are independently testable — implementing just
|
||||
one should leave you with something demonstrable.
|
||||
3. **Draw the boundaries.** For each slice, write one line of intent and an explicit
|
||||
scope boundary (what is in, what is deferred to a sibling slice). Sharp
|
||||
boundaries are what keep each sub-spec small enough to fit in context.
|
||||
4. **Order by dependency.** Note which slices depend on others and sequence them so
|
||||
prerequisites come first. Slices with no dependency on each other can be built in
|
||||
any order. To build independent slices in parallel, use separate worktrees so each
|
||||
run has isolated active-feature state.
|
||||
5. **Record the result as a roadmap.** Capture the slices in a durable roadmap file
|
||||
(below) so every later sub-spec can point back to it.
|
||||
|
||||
The roadmap is deliberately shallow: it names and orders the sub-features but does
|
||||
**not** design them. The design happens when each slice runs through its own
|
||||
`/speckit.specify`.
|
||||
|
||||
## The roadmap artifact
|
||||
|
||||
The roadmap is an ordinary Markdown file you author and keep under version control —
|
||||
there is no special tooling behind it. Put it where the sub-specs can find it:
|
||||
|
||||
- For a feature-scoped epic: `specs/<epic-slug>/roadmap.md`.
|
||||
- For a larger, cross-cutting epic: a top-level `ROADMAP.md`.
|
||||
|
||||
Each roadmap entry carries a stable id (used later for linking), a name, its intent,
|
||||
its scope boundary, its dependencies, a status, and — once the sub-spec exists — a
|
||||
link to it. A minimal template:
|
||||
|
||||
```markdown
|
||||
# Roadmap: <epic name>
|
||||
|
||||
<One or two sentences: what the epic is and why it is being decomposed.>
|
||||
|
||||
**Status legend**: planned · in-progress · done
|
||||
|
||||
| ID | Sub-feature | Intent | Scope boundary | Depends on | Status | Sub-spec |
|
||||
|----|-------------|--------|----------------|-----------|--------|----------|
|
||||
| R1 | <name> | <one line> | <in / deferred> | — | planned | — |
|
||||
| R2 | <name> | <one line> | <in / deferred> | R1 | planned | — |
|
||||
| R3 | <name> | <one line> | <in / deferred> | R1 | planned | — |
|
||||
```
|
||||
|
||||
Keep the `ID` column immutable once a sub-spec references it — it is the anchor for
|
||||
traceability. Fill in the `Sub-spec` column with the path to each sub-feature's spec
|
||||
directory as you create it, and update `Status` as work progresses.
|
||||
|
||||
## Specifying each sub-feature
|
||||
|
||||
With the roadmap in hand, work through the entries one at a time using the normal
|
||||
Spec Kit flow — nothing new to learn:
|
||||
|
||||
1. Pick the next roadmap entry whose dependencies are already `done` (or have none).
|
||||
2. Run `/speckit.specify` for just that slice, describing only its intent and scope
|
||||
from the roadmap entry. Because the slice is bounded, its spec, plan, and tasks
|
||||
stay well within the context window.
|
||||
3. Run `/speckit.plan`, `/speckit.tasks`, and `/speckit.implement` for that slice as
|
||||
usual.
|
||||
4. Mark the roadmap entry `done` and move to the next one.
|
||||
|
||||
Each slice is a complete, independent Spec Kit feature with its own
|
||||
`spec.md`/`plan.md`/`tasks.md`. The roadmap is what ties them together.
|
||||
|
||||
## Linking sub-specs to the roadmap
|
||||
|
||||
To keep scope and intent from drifting across separate runs, every sub-spec
|
||||
references its roadmap entry, and the roadmap links back — a simple, greppable,
|
||||
bidirectional convention:
|
||||
|
||||
- **Sub-spec → roadmap.** In the sub-feature's `spec.md`, name the parent roadmap
|
||||
and entry id in the `Input` / summary line, for example:
|
||||
|
||||
```markdown
|
||||
**Input**: Parent roadmap: `specs/<epic>/roadmap.md` → entry **R3**. <feature description>
|
||||
```
|
||||
|
||||
- **Roadmap → sub-spec.** In the roadmap table, set the entry's `Sub-spec` column to
|
||||
the sub-feature's directory, e.g. `specs/<epic>-part-3/`.
|
||||
|
||||
Because both directions are plain text, you can trace any sub-spec back to its place
|
||||
in the epic (and find its siblings) with a quick search — no tooling, no metadata
|
||||
schema.
|
||||
|
||||
## Keeping the roadmap and sub-specs in sync
|
||||
|
||||
The roadmap is a living document. As you learn more, keep it and the sub-specs
|
||||
aligned:
|
||||
|
||||
- **Roadmap first, then reconcile.** When scope shifts, update the roadmap entry
|
||||
first, then update any sub-specs it affects. The roadmap is the source of truth for
|
||||
how the epic is divided.
|
||||
- **Respect dependencies and ordering.** If a slice depends on another, build the
|
||||
prerequisite first and cross-reference the dependent sub-spec so the relationship
|
||||
is visible from both sides.
|
||||
- **Recurse when a slice is still too big.** If a sub-feature turns out to be too
|
||||
large to specify in one cycle, give it its own roadmap and decompose it further —
|
||||
the same approach applies one level down. Recursion adds overhead, so only go as
|
||||
deep as the context problem actually requires.
|
||||
|
||||
## Worked example
|
||||
|
||||
Suppose the epic is **"Add a self-service billing portal"** — far too large for a
|
||||
single cycle. The roadmap pass breaks it into three independently-specifiable
|
||||
slices.
|
||||
|
||||
`specs/billing-portal/roadmap.md`:
|
||||
|
||||
```markdown
|
||||
# Roadmap: Self-service billing portal
|
||||
|
||||
Let customers view invoices, manage payment methods, and change plans without
|
||||
contacting support. Too large for one cycle, so it is split into independent slices.
|
||||
|
||||
**Status legend**: planned · in-progress · done
|
||||
|
||||
| ID | Sub-feature | Intent | Scope boundary | Depends on | Status | Sub-spec |
|
||||
|----|--------------------|------------------------------------------|---------------------------------------------|-----------|---------|----------|
|
||||
| R1 | Invoice history | Customers view and download past invoices | Read-only; no payment actions | — | done | specs/billing-invoices/ |
|
||||
| R2 | Payment methods | Add, remove, and set a default card | No plan changes; assumes invoices exist | R1 | in-progress | specs/billing-payment-methods/ |
|
||||
| R3 | Plan changes | Upgrade/downgrade the subscription plan | Uses R2's default payment method | R1, R2 | planned | — |
|
||||
```
|
||||
|
||||
Each slice is then specified on its own. For example, the **R2** sub-feature's
|
||||
`spec.md` opens with a back-reference:
|
||||
|
||||
```markdown
|
||||
# Feature Specification: Billing — payment methods
|
||||
|
||||
**Input**: Parent roadmap: `specs/billing-portal/roadmap.md` → entry **R2**.
|
||||
Let customers add, remove, and set a default payment method in the billing portal.
|
||||
```
|
||||
|
||||
From here a reader can trace **R2** back to the roadmap, see that it depends on
|
||||
**R1** (invoice history, already `done`), and see that **R3** (plan changes) is
|
||||
waiting on it. Building R1, then R2, then R3 keeps every run small while the roadmap
|
||||
preserves the shape of the whole epic.
|
||||
|
||||
## For automation (optional)
|
||||
|
||||
If you would rather automate roadmap capture and consistency checks than maintain
|
||||
the file by hand, the community-maintained
|
||||
[Spec Roadmap extension](https://github.com/srobroek/speckit-roadmap) explores that
|
||||
direction. It is a third-party extension and is not required — the manual convention
|
||||
above is enough on its own.
|
||||
@@ -77,9 +77,9 @@ specify init <project_name> --integration pi
|
||||
specify init <project_name> --integration omp
|
||||
```
|
||||
|
||||
### Specify Script Type (Shell, PowerShell, or Python)
|
||||
### Specify Script Type (Shell vs PowerShell)
|
||||
|
||||
Automation scripts are available as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants.
|
||||
All automation scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants.
|
||||
|
||||
Auto behavior:
|
||||
|
||||
@@ -92,7 +92,6 @@ Force a specific script type:
|
||||
```bash
|
||||
specify init <project_name> --script sh
|
||||
specify init <project_name> --script ps
|
||||
specify init <project_name> --script py
|
||||
```
|
||||
|
||||
### Ignore Agent Tools Check
|
||||
@@ -132,7 +131,6 @@ Scripts are installed into a variant subdirectory matching the chosen script typ
|
||||
|
||||
- `.specify/scripts/bash/` — contains `.sh` scripts (default on Linux/macOS)
|
||||
- `.specify/scripts/powershell/` — contains `.ps1` scripts (default on Windows)
|
||||
- `.specify/scripts/python/` — contains `.py` scripts (chosen with `--script py`; also installs the platform shell fallback)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This guide shows how to iterate on the `specify` CLI locally without publishing a release or committing to `main` first.
|
||||
|
||||
> Scripts are available as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants. Interactive `specify init` prompts you to choose one; non-interactive runs default to a shell variant for your OS. Pass `--script sh|ps|py` to select explicitly.
|
||||
> Scripts now have both Bash (`.sh`) and PowerShell (`.ps1`) variants. The CLI auto-selects based on OS unless you pass `--script sh|ps`.
|
||||
|
||||
## 1. Clone and Switch Branches
|
||||
|
||||
@@ -189,7 +189,7 @@ rm -rf .venv dist build *.egg-info
|
||||
| `ModuleNotFoundError: typer` | Run `uv pip install -e .` |
|
||||
| Scripts not executable (Linux) | Re-run init or `chmod +x scripts/*.sh` |
|
||||
| Git commands unavailable | Install the git extension with `specify extension add git` |
|
||||
| Wrong script type downloaded | Pass `--script sh`, `--script ps`, or `--script py` explicitly |
|
||||
| Wrong script type downloaded | Pass `--script sh` or `--script ps` explicitly |
|
||||
| TLS errors on corporate network | Configure your environment's certificate store or proxy. The `--skip-tls` flag is deprecated and has no effect. |
|
||||
|
||||
## 14. Next Steps
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
This guide will help you get started with Spec-Driven Development using Spec Kit. Throughout, we illustrate each step with a running example: **Taskify**, a small team productivity platform.
|
||||
|
||||
> [!NOTE]
|
||||
> Automation scripts are provided as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants. Interactive `specify init` prompts you to choose one; non-interactive runs default to a shell variant for your OS. Pass `--script sh|ps|py` to select explicitly.
|
||||
> Automation scripts are provided as both Bash (`.sh`) and PowerShell (`.ps1`) variants. The `specify` CLI auto-selects based on your OS unless you pass `--script sh|ps`.
|
||||
|
||||
> [!NOTE]
|
||||
> Commands are shown here in `/speckit.*` form, but the exact invocation depends on your agent. Some skills-based agents use `$speckit-*` (e.g. Codex, ZCode) or `/skill:speckit-*` (e.g. Kimi). Use whichever form your agent exposes — the steps are otherwise identical.
|
||||
|
||||
@@ -12,7 +12,7 @@ specify init [<project_name>]
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `--integration <key>` | AI coding agent integration to use (e.g. `copilot`, `claude`, `gemini`). See the [Integrations reference](integrations.md) for all available keys |
|
||||
| `--integration-options` | Options for the integration (e.g. `--integration-options="--commands-dir .myagent/cmds"`) |
|
||||
| `--script sh\|ps\|py` | Script type: `sh` (bash/zsh), `ps` (PowerShell), or `py` (Python) |
|
||||
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||
| `--here` | Initialize in the current directory instead of creating a new one |
|
||||
| `--force` | Force merge/overwrite when initializing in an existing directory |
|
||||
| `--ignore-agent-tools` | Skip checks for AI coding agent CLI tools |
|
||||
|
||||
@@ -6,7 +6,6 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
|
||||
| Agent | Key | Notes |
|
||||
| ------------------------------------------------------------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Alquimia AI](https://docs.alquimia.ai) | `alquimia` | Skills-based integration; installs skills into `.alquimia/skills` and invokes them as `/speckit-<command>` |
|
||||
| [Amp](https://ampcode.com/) | `amp` | |
|
||||
| [Antigravity (agy)](https://antigravity.google/) | `agy` | Skills-based integration; skills are installed automatically |
|
||||
| [Auggie CLI](https://docs.augmentcode.com/cli/overview) | `auggie` | |
|
||||
@@ -16,7 +15,6 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
| [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>` |
|
||||
| [Factory Droid](https://docs.factory.ai/cli/getting-started/overview) | `droid` | Skills-based integration; installs skills into `.factory/skills/` and invokes them as `/speckit-<command>` |
|
||||
| [Firebender](https://firebender.com/) | `firebender` | IDE-based agent for Android Studio / IntelliJ |
|
||||
| [Forge](https://forgecode.dev/) | `forge` | |
|
||||
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
|
||||
@@ -26,7 +24,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
|
||||
| [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` | Skills-based integration by default; installs skills as `speckit-<command>/SKILL.md` under `.bob/skills/` and invokes them as `/speckit-<command>`. Pass `--integration-options="--legacy-commands"` to scaffold the deprecated Bob 1.x layout (`.bob/commands/*.md`) instead; that flag will be removed in a future release. Existing legacy installs can migrate with `specify integration upgrade bob --integration-options="--skills"`, which converts them to the skills layout and removes the old command files. If preset overrides are installed, the migration is rejected with an actionable error (preset artifacts cannot yet be reconciled across a layout change) — remove the preset(s), migrate, then reinstall them. |
|
||||
| [Junie](https://junie.jetbrains.com/) | `junie` | |
|
||||
| [Kilo Code](https://github.com/Kilo-Org/kilocode) | `kilocode` | Installs commands into `.kilo/commands`; legacy `.kilocode/workflows` installs remain supported as a registration fallback |
|
||||
| [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 |
|
||||
| [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 |
|
||||
@@ -87,7 +85,7 @@ specify integration install <key>
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `--script sh\|ps\|py` | Script type: `sh` (bash/zsh), `ps` (PowerShell), or `py` (Python) |
|
||||
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||
| `--force` | Opt in to installing alongside integrations that are not declared multi-install safe |
|
||||
| `--integration-options` | Integration-specific options (e.g. `--integration-options="--commands-dir .myagent/cmds"`) |
|
||||
|
||||
@@ -95,8 +93,6 @@ Installs the specified integration into the current project. If another integrat
|
||||
|
||||
Installing an additional integration does not change the default integration. Use `specify integration use <key>` to change the default.
|
||||
|
||||
Installed extensions and presets are not registered for a non-default integration at install time — they follow the currently active (default) integration only. `specify integration use <key>` (or `switch <key>`) is what rescaffolds them for the newly active integration.
|
||||
|
||||
> **Note:** All integration management commands require a project already initialized with `specify init`. To start a new project with a specific agent, use `specify init <project> --integration <key>` instead.
|
||||
|
||||
**Version note:** Controlled multi-install support was introduced in Spec Kit 0.8.5. If `specify integration install <key>` says another integration is already installed and only suggests `switch` or `uninstall`, check your local CLI with `specify version` and upgrade it. Running a one-shot command such as `uvx --from git+https://github.com/github/spec-kit.git specify ...` uses a temporary copy for that command only; it does not update the persistent `specify` executable on your `PATH`.
|
||||
@@ -125,12 +121,12 @@ specify integration switch <key>
|
||||
|
||||
| Option | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `--script sh\|ps\|py` | Script type: `sh` (bash/zsh), `ps` (PowerShell), or `py` (Python) |
|
||||
| `--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>`. Like `use`, `switch` rescaffolds installed extensions and presets for the target integration once it becomes the default.
|
||||
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>`.
|
||||
|
||||
## Use an Installed Integration
|
||||
|
||||
@@ -144,8 +140,6 @@ specify integration use <key>
|
||||
|
||||
Sets the default integration without uninstalling any other installed integrations. This also refreshes managed shared templates so command references match the new default integration's invocation style. Modified or untracked shared templates are preserved unless `--force` is used.
|
||||
|
||||
`use` is also the activation point for installed extensions and presets: it re-registers every enabled extension's and preset's command overrides (and skills, for skills-mode agents) for the newly active integration, so artifacts installed while a different integration was active are rescaffolded here rather than at install time.
|
||||
|
||||
## Upgrade an Integration
|
||||
|
||||
```bash
|
||||
@@ -155,15 +149,11 @@ specify integration upgrade [<key>]
|
||||
| Option | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `--force` | Overwrite files even if they have been modified |
|
||||
| `--script sh\|ps\|py` | Script type: `sh` (bash/zsh), `ps` (PowerShell), or `py` (Python) |
|
||||
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
|
||||
| `--integration-options` | Options for the integration |
|
||||
|
||||
Reinstalls an installed integration with updated templates and commands (e.g., after upgrading Spec Kit). Defaults to the default integration; if a key is provided, it must be one of the installed integrations. Detects locally modified files and blocks the upgrade unless `--force` is used. Stale files from the previous install that are no longer needed are removed automatically. Shared templates stay aligned with the default integration even when upgrading a non-default integration.
|
||||
|
||||
Enabled extensions and presets are re-registered only when upgrading the currently active (default) integration. A non-default upgrade still refreshes that integration's core commands, but does not re-register its extension or preset layers — `use`/`switch` that integration afterward to rescaffold them.
|
||||
|
||||
If an upgrade would change an integration between command and skills layouts while preset artifacts are registered for it, the upgrade is rejected before changing files. Remove the affected presets, run the layout-changing upgrade, then reinstall them.
|
||||
|
||||
## Report Integration Status
|
||||
|
||||
```bash
|
||||
@@ -266,35 +256,31 @@ Spec Kit tracks one default integration in `.specify/integration.json` with `def
|
||||
|
||||
An integration is multi-install safe when it uses a static, unique agent root and command directory, stable command invocation settings, and a separate install manifest whose managed files do not overlap another safe integration. Registry tests enforce those path and manifest invariants. Shared Spec Kit templates remain aligned to the single default integration.
|
||||
|
||||
The Command directory column below lists the directory each integration installs its commands or skills into. Context-file targeting is a separate concern from integration multi-install safety: `multi_install_safe` is an integration declaration about command/skill paths, whereas the optional agent-context extension manages a per-agent context file (for example `AGENTS.md` or `CLAUDE.md`) and can even synchronize several anchors at once via its `context_files` setting. Multiple agents mapping to the same context file is expected there and does not affect whether an integration is multi-install safe; see the agent-context extension for details.
|
||||
The Isolation column below lists paths Spec Kit manages for that integration (skills/commands roots and any integration-owned rule files). It is not a full inventory of every file an agent may read.
|
||||
|
||||
**Agent-context defaults are separate.** The optional agent-context extension maps each integration to a default context file in `extensions/agent-context/agent-context-defaults.json`. Those defaults are independent of multi-install safety: several agents may share a root file such as `AGENTS.md` when the extension is enabled. Multi-install safety does not require a unique context file per safe integration.
|
||||
|
||||
The currently declared multi-install safe integrations are:
|
||||
|
||||
| Key | Command directory |
|
||||
| --- | ----------------- |
|
||||
| `alquimia` | `.alquimia/skills` |
|
||||
| `auggie` | `.augment/commands` |
|
||||
| `claude` | `.claude/skills` |
|
||||
| `cline` | `.clinerules/workflows` |
|
||||
| `codebuddy` | `.codebuddy/commands` |
|
||||
| `codex` | `.agents/skills` |
|
||||
| `cursor-agent` | `.cursor/skills` |
|
||||
| `droid` | `.factory/skills` |
|
||||
| `firebender` | `.firebender/commands` |
|
||||
| `gemini` | `.gemini/commands` |
|
||||
| Key | Isolation |
|
||||
| --- | --------- |
|
||||
| `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` |
|
||||
| `grok` | `.grok/skills` |
|
||||
| `junie` | `.junie/commands` |
|
||||
| `kilocode` | `.kilo/commands` |
|
||||
| `kiro-cli` | `.kiro/prompts` |
|
||||
| `lingma` | `.lingma/skills` |
|
||||
| `omp` | `.omp/commands` |
|
||||
| `pi` | `.pi/prompts` |
|
||||
| `qodercli` | `.qoder/commands` |
|
||||
| `qwen` | `.qwen/commands` |
|
||||
| `shai` | `.shai/commands` |
|
||||
| `tabnine` | `.tabnine/agent/commands` |
|
||||
| `trae` | `.trae/skills` |
|
||||
| `zcode` | `.zcode/skills` |
|
||||
| `junie` | `.junie/commands`, `.junie/AGENTS.md` |
|
||||
| `kilocode` | `.kilocode/workflows`, `.kilocode/rules/specify-rules.md` |
|
||||
| `qodercli` | `.qoder/commands`, `QODER.md` |
|
||||
| `qwen` | `.qwen/commands`, `QWEN.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` |
|
||||
|
||||
Integrations that share a 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`.
|
||||
|
||||
@@ -313,7 +299,3 @@ CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be ins
|
||||
### When should I use `upgrade` vs `switch`?
|
||||
|
||||
Use `upgrade` when you've upgraded Spec Kit and want to refresh an installed integration's managed files. Use `switch` when you want to replace the current default with another integration; if the target is already installed, `switch` behaves like `use`.
|
||||
|
||||
### Do extensions and presets I install apply to every installed integration?
|
||||
|
||||
No. Extensions (`specify extension add`) and presets (`specify preset add`) register their command overrides for the currently active (default) integration only, even if other integrations are installed. A non-default integration does not receive those artifacts until it becomes the default: `specify integration use <key>` (or `switch <key>`) rescaffolds every enabled extension and preset for the newly active integration. `specify integration upgrade` follows the same rule — it only re-registers extensions and presets when upgrading the active integration.
|
||||
|
||||
@@ -139,7 +139,7 @@ catalogs:
|
||||
|
||||
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.
|
||||
|
||||
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 the active integration's directory only, instead of being re-resolved by agents or written to every detected agent directory (#2948). During preset install, Spec Kit registers command files for the preset being installed against the currently active integration; post-install and post-removal reconciliation then recomputes and writes the effective command content for affected command names based on the active stack. Install and rescaffold remain active-only, but removal may also update previously targeted inactive directories recorded by the removed preset to restore the surviving command or skill layer. A non-active installed integration does not otherwise receive these command files until it becomes the default — `specify integration use <key>` (or `switch <key>`) rescaffolds enabled presets for the newly active integration. Agents do not re-resolve the stack each time they run a command.
|
||||
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.
|
||||
|
||||
|
||||
@@ -39,21 +39,6 @@ specify workflow run my-pipeline.yml --json
|
||||
|
||||
`workflow_id` is the `workflow.id` declared inside the YAML, not the file name. The object is printed exactly as shown — pretty-printed with two-space indentation, on plain stdout with no Rich markup — so it always parses. While the workflow runs under `--json`, any progress a step would print (for example a gate prompt, or output from a prompt step's CLI subprocess) is redirected to stderr, so stdout carries only the JSON object. Read the object from stdout; leave stderr attached to the terminal or capture it separately.
|
||||
|
||||
For `failed` and `aborted` runs, the payload includes an `error` field carrying the terminal step's error message:
|
||||
|
||||
```json
|
||||
{
|
||||
"run_id": "662bf791",
|
||||
"workflow_id": "build-and-review",
|
||||
"status": "failed",
|
||||
"current_step_id": "boom",
|
||||
"current_step_index": 0,
|
||||
"error": "Command exited with code 3"
|
||||
}
|
||||
```
|
||||
|
||||
`completed` and `paused` runs omit the `error` field. The error is persisted in the run's `state.json`, so `specify workflow status <run_id> --json` surfaces the same message after the fact.
|
||||
|
||||
> **Note:** Most workflow commands require a project already initialized with `specify init`. The exception is `specify workflow run <local-file.{yml,yaml}>`, which can run outside a project; in that case, run state is stored under the current directory's `.specify/workflows/runs/<run_id>/`.
|
||||
|
||||
## Resume a Workflow
|
||||
@@ -103,17 +88,10 @@ specify workflow add <source>
|
||||
|
||||
| Option | Description |
|
||||
| --------------- | ------------------------------------------------------ |
|
||||
| `--dev` | Install from a local YAML file, package directory, or archive |
|
||||
| `--dev` | Install from a local workflow YAML file or directory |
|
||||
| `--from <url>` | Install from a custom URL (`<source>` names the expected workflow ID) |
|
||||
|
||||
Installs a workflow from the catalog, an HTTPS URL, a local YAML file, a
|
||||
directory containing `workflow.yml`, or a `.zip`, `.tar.gz`, or `.tgz`
|
||||
archive. Archives may contain `workflow.yml` at the root or inside one
|
||||
top-level directory.
|
||||
|
||||
Directory and archive installs preserve the complete workflow package,
|
||||
including scripts and other companion files. ZIP, `.tar.gz`, and `.tgz`
|
||||
archives follow the same validation and installation behavior.
|
||||
Installs a workflow from the catalog, a URL (HTTPS required), a local YAML file, or a local directory containing `workflow.yml`.
|
||||
|
||||
## Workflow Overlays
|
||||
|
||||
@@ -288,9 +266,7 @@ Lower priority values have higher precedence. Change this overlay to `priority:
|
||||
|
||||
### Interaction with Bundles and Updates
|
||||
|
||||
`specify workflow add <local-directory>` installs the complete local workflow
|
||||
package into `.specify/workflows/<id>/`. Archive installs preserve the same
|
||||
package contents.
|
||||
`specify workflow add <local-directory>` installs `workflow.yml` from the local directory into `.specify/workflows/<id>/`.
|
||||
|
||||
When an installed workflow is refreshed or reinstalled, project overlays in `.specify/workflows/overlays/<id>/` are preserved because they live outside the installed workflow directory.
|
||||
|
||||
@@ -526,32 +502,6 @@ args: "{{ inputs.spec }}"
|
||||
message: "{{ status | default('pending') }}"
|
||||
```
|
||||
|
||||
### Interpolation and shell safety
|
||||
|
||||
Expressions are resolved by **plain string substitution** — the value of `{{ ... }}` is spliced into the surrounding text exactly as-is, with no quoting or escaping added. That is convenient for building `args` and `message` strings, but it has an important consequence for `shell` steps: a `run` field is handed to the system shell (`/bin/sh -c` on POSIX), so any interpolated value is interpreted as **shell syntax**, not just data.
|
||||
|
||||
If an interpolated value can contain characters like `;`, `|`, `&`, `$( )`, backticks, or quotes, it can change or extend the command that actually runs. This matters most when the value is not fully under the workflow author's control:
|
||||
|
||||
- **Workflow `inputs.*`** — supplied by whoever runs the workflow.
|
||||
- **A prior step's output**, e.g. `{{ steps.plan.output.stdout }}` — for a `prompt` step this is **text produced by the AI agent**, which can in turn be influenced by files, tickets, or web content the agent read. Treat agent output as untrusted when it flows into a `shell` step.
|
||||
|
||||
There is **no shell-escaping filter** in the expression language and **no sandbox** around a `shell` step, so none of the practices below can be treated as a guarantee that a hostile value is neutralised. The only reliable control is to constrain what an interpolated value *can* be, and to keep values you cannot constrain out of `run` fields entirely. Scrutinise every `run` field that interpolates a value you do not control, and at minimum:
|
||||
|
||||
- **Constrain the value at the source with `enum`/an allowlist.** When `inputs.*` feeds a `run` field, restrict it to a fixed set of known-safe values so a caller cannot supply arbitrary shell text at all. This is the strongest control the engine offers — prefer it over any downstream mitigation.
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
target:
|
||||
type: string
|
||||
enum: [staging, production] # caller cannot inject arbitrary text
|
||||
```
|
||||
|
||||
- **Keep unconstrained values out of `run`.** If a value cannot be constrained to an allowlist — most agent/`prompt` output — do not interpolate it into a `run` field. Branch on it with `if`/`switch` against fixed conditions, or act on it in a `command`/`prompt` step rather than a shell command built from it.
|
||||
- **Quoting is not a security boundary.** Surrounding a substitution with quotes (`'{{ inputs.x }}'`) helps the shell treat a *trusted* value as a single argument and avoids word-splitting on spaces, but a value that itself contains the matching quote character can still break out and inject shell syntax. Quote for correctness on constrained values; never rely on quoting to make an *unconstrained* substitution safe.
|
||||
- **Gates do not inspect the next step, and `message` is printed verbatim.** A `gate` step renders only its own `message`/`show_file` — it does not display, resolve, or sanitise the command that follows it, and approval never neutralises an injectable interpolation. Do **not** interpolate raw untrusted data into `message`: it is printed as-is with no control-character stripping, so agent or caller output could inject terminal/ANSI escapes that alter or hide the approval prompt. Keep `message` to trusted, constrained text, and surface untrusted material for review via `show_file` instead — its path and contents are control/ANSI-stripped before display.
|
||||
|
||||
A `shell` step is an arbitrary-command primitive by design; these practices reduce exposure and keep *which* command runs under the author's control, but they do not eliminate the risk of interpolating values you do not fully control.
|
||||
|
||||
## Shell Step Environment Variables
|
||||
|
||||
Shell steps automatically receive the following environment variables:
|
||||
@@ -578,51 +528,6 @@ Each workflow run persists its state at `.specify/workflows/runs/<run_id>/`:
|
||||
|
||||
This enables `specify workflow resume` to continue from the exact step where a run was paused (e.g., at a gate) or failed.
|
||||
|
||||
### Gate Verdict Inputs
|
||||
|
||||
`verdict_input` binds a gate's verdict to a named workflow input. The input must be declared in the workflow's `inputs` block; `specify workflow validate` reports an undeclared reference.
|
||||
|
||||
`verdict_input` is not supported inside a `fan-out` template. Fan-out items
|
||||
share workflow inputs, while workflow state can represent only one paused
|
||||
gate. Place a gate before the fan-out to approve the whole batch, or after a
|
||||
fan-in to review the aggregated results.
|
||||
|
||||
**Input value semantics:**
|
||||
|
||||
| Value | Behavior |
|
||||
|---|---|
|
||||
| Non-empty string, matches an option (case-insensitive) | Gate auto-decides; `output.choice` is set to the configured option spelling |
|
||||
| Non-empty string, no match | Gate fails immediately |
|
||||
| Non-string | Gate fails immediately |
|
||||
| Missing or empty | Gate prompts on a TTY; pauses otherwise |
|
||||
|
||||
**Default value semantics:** A non-empty `default` is consumed as a verdict on the first run — matching an option auto-decides the gate, not matching fails it immediately.
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
spec_verdict:
|
||||
type: string
|
||||
default: ""
|
||||
steps:
|
||||
- id: review-spec
|
||||
type: gate
|
||||
message: "Approve the specification?"
|
||||
options: [approve, reject]
|
||||
on_reject: retry
|
||||
verdict_input: spec_verdict
|
||||
```
|
||||
|
||||
Supply a verdict when resuming:
|
||||
|
||||
```bash
|
||||
specify workflow resume <run_id> --input spec_verdict=approve
|
||||
```
|
||||
|
||||
For `on_reject: retry`, a bound reject verdict is consumed before the gate
|
||||
pauses: the named stored input is reset to `""`. A later resume therefore
|
||||
prompts or pauses again until another verdict is supplied. Approve, abort, and
|
||||
skip outcomes leave the input unchanged.
|
||||
|
||||
## FAQ
|
||||
|
||||
### What happens when a workflow hits a gate step?
|
||||
|
||||
@@ -55,8 +55,6 @@
|
||||
href: concepts/spec-persistence.md
|
||||
- name: Handling Complex Features
|
||||
href: concepts/complex-features.md
|
||||
- name: Spec of Specs
|
||||
href: concepts/spec-of-specs.md
|
||||
|
||||
# Development workflows
|
||||
- name: Development
|
||||
|
||||
266
docs/upgrade.md
266
docs/upgrade.md
@@ -12,7 +12,7 @@
|
||||
| **CLI Tool — pin a version** | `specify self upgrade --tag vX.Y.Z[suffix]` | Upgrade to a specific release tag instead of the latest stable. Suffixes are limited to dev, alpha/beta/rc, and/or build metadata forms. |
|
||||
| **CLI Tool — manual fallback** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | When `specify self upgrade` isn't available (older installs) or when you want explicit control. |
|
||||
| **CLI Tool — manual fallback (pipx)** | `pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z` | Same as above, for pipx installs. |
|
||||
| **Project Files** | Run `specify integration upgrade <key>`, then `specify extension update` | Refresh installed integration files and extensions in your project |
|
||||
| **Project Files** | `specify init --here --force --integration <your-agent>` | Update slash commands, templates, and scripts in your project |
|
||||
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
|
||||
|
||||
---
|
||||
@@ -89,94 +89,91 @@ specify self check
|
||||
|
||||
## Part 2: Updating Project Files
|
||||
|
||||
When Spec Kit releases new features (like new slash commands, updated templates, or extension changes), you need to refresh the Spec Kit files that were installed into your project.
|
||||
When Spec Kit releases new features (like new slash commands or updated templates), you need to refresh your project's Spec Kit files.
|
||||
|
||||
### What gets updated?
|
||||
|
||||
For existing Spec Kit projects, use the manifest-aware upgrade path first:
|
||||
Running `specify init --here --force` will update:
|
||||
|
||||
- ✅ **Integration command/skill files** (`.claude/skills/`, `.github/prompts/`, `.agents/skills/`, etc.)
|
||||
- ✅ **Managed shared scripts and templates** (`.specify/scripts/`, `.specify/templates/`) when they are unchanged from the previous managed copy
|
||||
- ✅ **Installed extensions** when you run `specify extension update`
|
||||
|
||||
The integration upgrade command uses the install manifest to detect local edits. If a managed integration file was modified after install, the command stops and asks you to inspect the change or rerun with `--force`.
|
||||
- ✅ **Slash command files** (`.claude/commands/`, `.github/prompts/`, etc.)
|
||||
- ✅ **Script files** (`.specify/scripts/`) — **only with `--force`**; without it, only missing files are added
|
||||
- ✅ **Template files** (`.specify/templates/`) — **only with `--force`**; without it, only missing files are added
|
||||
- ✅ **Shared memory files** (`.specify/memory/`) - **⚠️ See warnings below**
|
||||
|
||||
### What stays safe?
|
||||
|
||||
These files are **never touched** by the manifest-aware integration/extension upgrade path:
|
||||
These files are **never touched** by the upgrade—the template packages don't even contain them:
|
||||
|
||||
- ✅ **Your specifications** (`specs/001-my-feature/spec.md`, etc.) - **CONFIRMED SAFE**
|
||||
- ✅ **Your implementation plans** (`specs/001-my-feature/plan.md`, `tasks.md`, etc.) - **CONFIRMED SAFE**
|
||||
- ✅ **Your constitution** (`.specify/memory/constitution.md`) when using `specify integration upgrade`
|
||||
- ✅ **Your source code** - **CONFIRMED SAFE**
|
||||
- ✅ **Your git history** - **CONFIRMED SAFE**
|
||||
|
||||
The `specs/` directory is completely excluded from template packages and will never be modified during upgrades.
|
||||
|
||||
### 1. Check installed integrations
|
||||
### Update command
|
||||
|
||||
Run this inside your project directory:
|
||||
|
||||
```bash
|
||||
specify integration status
|
||||
```
|
||||
|
||||
This reports the default integration, all installed integrations, and any modified or missing managed files. You can also inspect `.specify/integration.json`; installed integrations are listed under `installed_integrations`.
|
||||
|
||||
### 2. Upgrade each installed integration
|
||||
|
||||
Run this inside your project directory:
|
||||
|
||||
```bash
|
||||
specify integration upgrade <key>
|
||||
```
|
||||
|
||||
Replace `<key>` with an installed integration key such as `copilot`, `claude`, or `codex`. In projects with multiple installed integrations, run the command once per installed key.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
specify integration upgrade claude
|
||||
specify integration upgrade codex
|
||||
```
|
||||
|
||||
See the [integration reference](reference/integrations.md#upgrade-an-integration) for options such as `--script`, `--integration-options`, and `--force`.
|
||||
|
||||
### 3. Update installed extensions
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
specify extension update
|
||||
```
|
||||
|
||||
With no extension argument, this updates all installed extensions. Use `specify extension update <extension-id-or-name>` to update only one extension. See the [extensions reference](reference/extensions.md#update-extensions) for details.
|
||||
|
||||
### Fallback: re-run init
|
||||
|
||||
If a project predates manifests, has missing integration metadata, or needs a broader recovery, you can still re-run init:
|
||||
|
||||
```bash
|
||||
specify init --here --force --integration <your-agent>
|
||||
```
|
||||
|
||||
Use this as an escape hatch rather than the default project-file upgrade path. It refreshes the selected integration and shared project scaffolding, but it does not use the same per-integration manifest checks before overwriting files.
|
||||
Replace `<your-agent>` with your AI coding agent. Refer to this list of [Supported AI Coding Agent Integrations](reference/integrations.md)
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
specify init --here --force --integration copilot
|
||||
```
|
||||
|
||||
### Understanding the `--force` flag
|
||||
|
||||
Without `--force`, the CLI warns you and asks for confirmation:
|
||||
|
||||
```text
|
||||
Warning: Current directory is not empty (25 items)
|
||||
Template files will be merged with existing content and may overwrite existing files
|
||||
Proceed? [y/N]
|
||||
```
|
||||
|
||||
With `--force`, it skips the confirmation and proceeds immediately. It also **overwrites shared infrastructure files** (`.specify/scripts/` and `.specify/templates/`) with the latest versions from the installed Spec Kit release.
|
||||
|
||||
Without `--force`, shared infrastructure files that already exist are skipped — the CLI will print a warning listing the skipped files so you know which ones were not updated.
|
||||
|
||||
**Important: Your `specs/` directory is always safe.** The `--force` flag only affects template files (commands, scripts, templates, memory). Your feature specifications, plans, and tasks in `specs/` are never included in upgrade packages and cannot be overwritten.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Warnings
|
||||
|
||||
### 1. Constitution file and memory customizations
|
||||
### 1. Constitution file will be overwritten
|
||||
|
||||
`specify integration upgrade <key>` does not update `.specify/memory/constitution.md`.
|
||||
**Known issue:** `specify init --here --force` currently overwrites `.specify/memory/constitution.md` with the default template, erasing any customizations you made.
|
||||
|
||||
The fallback `specify init --here --force --integration <your-agent>` path also preserves an existing `.specify/memory/constitution.md`; if the file is missing, init creates it from the current constitution template. You do not need a constitution backup/restore step for the manifest-aware upgrade path.
|
||||
**Workaround:**
|
||||
|
||||
As with any broad fallback refresh, commit or back up local customizations before using `init --here --force` so you can review the resulting diff.
|
||||
```bash
|
||||
# 1. Back up your constitution before upgrading
|
||||
cp .specify/memory/constitution.md .specify/memory/constitution-backup.md
|
||||
|
||||
### 2. Custom integration, script, or template modifications
|
||||
# 2. Run the upgrade
|
||||
specify init --here --force --integration copilot
|
||||
|
||||
`specify integration upgrade <key>` blocks when manifest-tracked integration files were modified locally, unless you pass `--force`.
|
||||
# 3. Restore your customized constitution
|
||||
mv .specify/memory/constitution-backup.md .specify/memory/constitution.md
|
||||
```
|
||||
|
||||
Shared scripts and templates are refreshed when they still match the previously recorded managed copy. Local customizations are preserved unless you explicitly use a force/refresh option that overwrites them. If you customized files in `.specify/scripts/` or `.specify/templates/`, commit or back them up first:
|
||||
Or use git to restore it:
|
||||
|
||||
```bash
|
||||
# After upgrade, restore from git history
|
||||
git restore .specify/memory/constitution.md
|
||||
```
|
||||
|
||||
### 2. Custom script or template modifications
|
||||
|
||||
If you customized files in `.specify/scripts/` or `.specify/templates/`, the `--force` flag will overwrite them. Back them up first:
|
||||
|
||||
```bash
|
||||
# Back up custom templates and scripts
|
||||
@@ -195,87 +192,21 @@ Some IDE-based agents (like Kilo Code, Cline) may show **duplicate slash command
|
||||
**Example for Kilo Code:**
|
||||
|
||||
```bash
|
||||
# List current and legacy Kilo command folders
|
||||
ls -la .kilo/commands/
|
||||
ls -la .kilocode/workflows/
|
||||
# Navigate to the agent's commands folder
|
||||
cd .kilocode/workflows/
|
||||
|
||||
# List files and identify duplicates
|
||||
ls -la
|
||||
|
||||
# Delete old versions (example filenames - yours may differ)
|
||||
rm .kilocode/workflows/speckit.specify-old.md
|
||||
rm .kilocode/workflows/speckit.plan-v1.md
|
||||
rm speckit.specify-old.md
|
||||
rm speckit.plan-v1.md
|
||||
```
|
||||
|
||||
Restart your IDE to refresh the command list.
|
||||
|
||||
---
|
||||
|
||||
## Behavior change: `/constitution` no longer propagates into templates
|
||||
|
||||
The `/constitution` command ([#3790](https://github.com/github/spec-kit/pull/3790)) is scoped to
|
||||
its own artifact. It updates
|
||||
`.specify/memory/constitution.md` and writes a Sync Impact Report, and **no longer edits**
|
||||
`plan-template.md`, `spec-template.md`, `tasks-template.md`, installed command files, or
|
||||
guidance docs.
|
||||
|
||||
### Why
|
||||
|
||||
Spec Kit uses **runtime resolution**: `plan`, `tasks`, and `analyze` read
|
||||
`.specify/memory/constitution.md` live on every run, and `analyze` is the dedicated drift
|
||||
checker. The governed templates carry a pointer, not a copy — `plan-template.md` ships
|
||||
`[Gates determined based on constitution file]`, and `/plan` fills that section from the live
|
||||
constitution each run. Propagation duplicated the single source of truth and fought the
|
||||
preset/override composition system (a `replace` preset shadows an edited core template).
|
||||
|
||||
More broadly, presets and extensions — not in-place file edits — are how Spec Kit now governs
|
||||
shared assets. Composing policy through the resolution stack keeps it centrally owned, versioned,
|
||||
and auditable across repositories, instead of frozen into per-repo copies no core team can see.
|
||||
|
||||
### Is this a breaking change for existing projects?
|
||||
|
||||
**No — your workflow keeps working.** You would only notice a difference if you relied on
|
||||
`/constitution` editing those files in place. The templates are scaffolds, not authorities. When you
|
||||
run `/plan`, it copies the template into a per-feature `plan.md` and re-derives the Constitution
|
||||
Check from the live constitution; `/analyze` validates against it. Even if a previous
|
||||
`/constitution` run materialized concrete gate text into `.specify/templates/plan-template.md`,
|
||||
the live constitution remains the source of truth at runtime.
|
||||
|
||||
On a **non-forced upgrade**, a materialized template is *preserved* (its hash diverges from the
|
||||
recorded managed copy, so the refresh treats it as a customization and does not overwrite it).
|
||||
Nothing regresses.
|
||||
|
||||
### Optional cleanup — return to the runtime pointer
|
||||
|
||||
A frozen, pre-filled Constitution Check is a slightly misleading scaffold and can bias the first
|
||||
`/plan` pass. To move fully back to runtime resolution, reset the section body in
|
||||
`.specify/templates/plan-template.md` to the pointer:
|
||||
|
||||
```text
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
```
|
||||
|
||||
Leave the rest of the file untouched. This is cleanup, not a required migration.
|
||||
|
||||
### Keeping the old behavior (opt-in)
|
||||
|
||||
If your team treats the materialized templates as **reviewed, committed artifacts** and wants
|
||||
`/constitution` to keep propagating, install the bundled **`constitution-sync`** preset:
|
||||
|
||||
```bash
|
||||
specify preset add constitution-sync
|
||||
```
|
||||
|
||||
It wraps the core `/constitution` command and re-adds the propagation pass. It does **not** edit
|
||||
versioned preset- or extension-provided templates or command files (those are owned by their
|
||||
packages and are recomposed on reconciliation). Note that this edit-in-place propagation model
|
||||
conflicts with the composition model used by the rest of the SDD commands when they are
|
||||
preset/extension-managed — see the "Interaction with the resolution stack" section in
|
||||
`presets/constitution-sync/README.md` for the tradeoffs and when to prefer the default instead.
|
||||
|
||||
---
|
||||
|
||||
## Common Scenarios
|
||||
|
||||
### Scenario 1: "I just want new slash commands"
|
||||
@@ -284,29 +215,29 @@ preset/extension-managed — see the "Interaction with the resolution stack" sec
|
||||
# Upgrade CLI (auto-detects uv tool vs pipx install)
|
||||
specify self upgrade
|
||||
|
||||
# Inspect installed integrations
|
||||
specify integration status
|
||||
|
||||
# Update project files to get new commands
|
||||
specify integration upgrade <key>
|
||||
specify extension update
|
||||
specify init --here --force --integration copilot
|
||||
|
||||
# Restore your constitution if customized
|
||||
git restore .specify/memory/constitution.md
|
||||
```
|
||||
|
||||
### Scenario 2: "I customized templates and constitution"
|
||||
|
||||
```bash
|
||||
# 1. Commit or back up customizations
|
||||
git status
|
||||
# 1. Back up customizations
|
||||
cp .specify/memory/constitution.md /tmp/constitution-backup.md
|
||||
cp -r .specify/templates /tmp/templates-backup
|
||||
|
||||
# 2. Upgrade CLI
|
||||
specify self upgrade
|
||||
|
||||
# 3. Use the manifest-aware project update first
|
||||
specify integration upgrade <key>
|
||||
specify extension update
|
||||
# 3. Update project
|
||||
specify init --here --force --integration copilot
|
||||
|
||||
# 4. If the upgrade reports modified managed files, inspect the diff before using --force
|
||||
# 4. Restore customizations
|
||||
mv /tmp/constitution-backup.md .specify/memory/constitution.md
|
||||
# Manually merge template changes if needed
|
||||
```
|
||||
|
||||
### Scenario 3: "I see duplicate slash commands in my IDE"
|
||||
@@ -314,12 +245,14 @@ specify extension update
|
||||
This happens with IDE-based agents (Kilo Code, Cline, etc.).
|
||||
|
||||
```bash
|
||||
# For Kilo Code, inspect both current and legacy command folders
|
||||
ls -la .kilo/commands/
|
||||
ls -la .kilocode/workflows/
|
||||
# Find the agent folder (example: .kilocode/workflows/)
|
||||
cd .kilocode/workflows/
|
||||
|
||||
# List all files
|
||||
ls -la
|
||||
|
||||
# Delete old command files
|
||||
rm .kilocode/workflows/speckit.old-command-name.md
|
||||
rm speckit.old-command-name.md
|
||||
|
||||
# Restart your IDE
|
||||
```
|
||||
@@ -329,14 +262,14 @@ rm .kilocode/workflows/speckit.old-command-name.md
|
||||
The git extension is now opt-in, so upgrades do not install it unless you add it explicitly.
|
||||
|
||||
```bash
|
||||
# Upgrade CLI
|
||||
specify self upgrade
|
||||
# Manually back up files you customized
|
||||
cp .specify/memory/constitution.md .specify/memory/constitution.backup.md
|
||||
|
||||
# Refresh integration files and installed extensions
|
||||
specify integration upgrade <key>
|
||||
specify extension update
|
||||
# Run upgrade
|
||||
specify init --here --force --integration copilot
|
||||
|
||||
# The git extension is not added unless you run `specify extension add git`
|
||||
# Restore customizations
|
||||
mv .specify/memory/constitution.backup.md .specify/memory/constitution.md
|
||||
```
|
||||
|
||||
If you later decide you want the git extension's commands and hooks, install it explicitly:
|
||||
@@ -371,7 +304,7 @@ Alternatively, run the `/speckit.specify` command which creates `.specify/featur
|
||||
2. **For CLI-based agents**, verify files exist:
|
||||
|
||||
```bash
|
||||
ls -la .claude/skills/ # Claude Code
|
||||
ls -la .claude/commands/ # Claude Code
|
||||
ls -la .gemini/commands/ # Gemini
|
||||
ls -la .cursor/skills/ # Cursor
|
||||
ls -la .pi/prompts/ # Pi Coding Agent
|
||||
@@ -382,21 +315,19 @@ Alternatively, run the `/speckit.specify` command which creates `.specify/featur
|
||||
- Codex requires `CODEX_HOME` environment variable
|
||||
- Some agents need workspace restart or cache clearing
|
||||
|
||||
### "Will init overwrite my constitution customizations?"
|
||||
### "I lost my constitution customizations"
|
||||
|
||||
Current `specify init --here --force` preserves an existing `.specify/memory/constitution.md`; it creates the file from the template only when it is missing.
|
||||
|
||||
If you previously lost constitution changes through an older workflow or manual replacement, restore from git or backup:
|
||||
**Fix:** Restore from git or backup:
|
||||
|
||||
```bash
|
||||
# If you committed the customized constitution
|
||||
# If you committed before upgrading
|
||||
git restore .specify/memory/constitution.md
|
||||
|
||||
# If you backed up manually
|
||||
cp /tmp/constitution-backup.md .specify/memory/constitution.md
|
||||
```
|
||||
|
||||
**Prevention:** Use `specify integration upgrade <key>` for routine project-file updates. If you need the fallback `specify init --here --force` path, commit first so you can review the full diff afterward.
|
||||
**Prevention:** Always commit or back up `constitution.md` before upgrading.
|
||||
|
||||
### "Warning: Current directory is not empty"
|
||||
|
||||
@@ -420,10 +351,10 @@ This warning appears when you run `specify init --here` (or `specify init .`) in
|
||||
|
||||
Only Spec Kit infrastructure files:
|
||||
|
||||
- Agent command/skill files (`.claude/skills/`, `.github/prompts/`, etc.)
|
||||
- Agent command files (`.claude/commands/`, `.github/prompts/`, etc.)
|
||||
- Scripts in `.specify/scripts/`
|
||||
- Templates in `.specify/templates/`
|
||||
- Missing memory files such as `.specify/memory/constitution.md` may be created from templates; an existing constitution is preserved
|
||||
- Memory files in `.specify/memory/` (including constitution)
|
||||
|
||||
**What stays untouched:**
|
||||
|
||||
@@ -434,7 +365,7 @@ Only Spec Kit infrastructure files:
|
||||
|
||||
**How to respond:**
|
||||
|
||||
- **Type `y` and press Enter** - Proceed with the merge when using the fallback init path
|
||||
- **Type `y` and press Enter** - Proceed with the merge (recommended if upgrading)
|
||||
- **Type `n` and press Enter** - Cancel the operation
|
||||
- **Use `--force` flag** - Skip this confirmation entirely:
|
||||
|
||||
@@ -444,11 +375,11 @@ Only Spec Kit infrastructure files:
|
||||
|
||||
**When you see this warning:**
|
||||
|
||||
- ✅ **Expected** when using the fallback init path in an existing Spec Kit project
|
||||
- ✅ **Expected** when upgrading an existing Spec Kit project
|
||||
- ✅ **Expected** when adding Spec Kit to an existing codebase
|
||||
- ⚠️ **Unexpected** if you thought you were creating a new project in an empty directory
|
||||
|
||||
**Prevention tip:** Before using the fallback init path, commit your current work so any refreshed files are easy to review or restore.
|
||||
**Prevention tip:** Before upgrading, commit or back up your `.specify/memory/constitution.md` if you customized it.
|
||||
|
||||
### "CLI upgrade doesn't seem to work"
|
||||
|
||||
@@ -487,15 +418,14 @@ uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
|
||||
|
||||
### "Do I need to run specify every time I open my project?"
|
||||
|
||||
**Short answer:** No, you only run `specify init` once per project, or later as a fallback recovery path.
|
||||
**Short answer:** No, you only run `specify init` once per project (or when upgrading).
|
||||
|
||||
**Explanation:**
|
||||
|
||||
The `specify` CLI tool is used for:
|
||||
|
||||
- **Initial setup:** `specify init` to bootstrap Spec Kit in your project
|
||||
- **Routine project-file upgrades:** `specify integration upgrade <key>` and `specify extension update`
|
||||
- **Fallback recovery:** `specify init --here --force` when integration metadata is missing or the manifest-aware path cannot be used
|
||||
- **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.
|
||||
@@ -509,7 +439,7 @@ Once you've run `specify init`, the slash commands (like `/speckit.specify`, `/s
|
||||
ls -la .github/prompts/
|
||||
|
||||
# For Claude
|
||||
ls -la .claude/skills/
|
||||
ls -la .claude/commands/
|
||||
|
||||
# For Pi
|
||||
ls -la .pi/prompts/
|
||||
|
||||
@@ -2,55 +2,55 @@
|
||||
|
||||
This bundled extension manages the **coding agent context/instruction file** (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`, `GEMINI.md`, …) for the active integration.
|
||||
|
||||
It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `<!-- SPECKIT START -->` / `<!-- SPECKIT END -->`). For `.mdc` files, it also ensures the YAML frontmatter (the metadata block at the top of the file) contains `alwaysApply: true`. Otherwise, everything outside the managed section is untouched.
|
||||
|
||||
> NOTE: Spec Kit itself never touches your agent context file. This extension is the only thing that does, and it's opt-in: install it if you want the block kept in sync, skip it if you'd rather manage that file yourself.
|
||||
It owns the lifecycle of the managed section delimited by the configurable start/end markers (defaults: `<!-- SPECKIT START -->` / `<!-- SPECKIT END -->`).
|
||||
|
||||
## 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:
|
||||
|
||||
- **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; when it is absent, the file is never modified, and when it is disabled, its automatic hooks do not run.
|
||||
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` ([agent-context-config.yml](./agent-context-config.yml) in this repo) - the bundled scripts honor the `context_markers` value.
|
||||
- **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.
|
||||
- **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](./extension.yml) (`after_specify`, `after_plan`).
|
||||
|
||||
## Installation
|
||||
|
||||
To install the extension, from the root of an initialized Spec Kit project, run:
|
||||
|
||||
```bash
|
||||
specify extension add agent-context
|
||||
```
|
||||
|
||||
## Disabling
|
||||
|
||||
```bash
|
||||
specify extension disable agent-context
|
||||
|
||||
# Re-enable it
|
||||
specify extension enable agent-context
|
||||
```
|
||||
|
||||
While this extension is disabled (or not installed), nothing in Spec Kit creates, updates, or removes the managed block - the `__CONTEXT_FILE__` placeholder in any template is left as-is, and the extension's own config is never read.
|
||||
- **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).
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. |
|
||||
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).
|
||||
|
||||
> NOTE: The command ID above is canonical. Invoke it using the syntax for your integration: `/speckit.agent-context.update` for dot-command integrations; `/speckit-agent-context-update` for hyphen/skills integrations (including Forge and Cline); `$speckit-agent-context-update` for Codex or ZCode in skills mode; or `/skill:speckit-agent-context-update` for Kimi.
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `speckit.agent-context.update` | Refresh the managed section in the agent context file with the current plan path. |
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration flows through the extension's own config file at `.specify/extensions/agent-context/agent-context-config.yml` ([agent-context-config.yml](./agent-context-config.yml) in the repo).
|
||||
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
|
||||
context_file: CLAUDE.md
|
||||
|
||||
# Optional list of coding agent context files to manage together.
|
||||
# When non-empty, this takes precedence over context_file.
|
||||
context_files:
|
||||
- AGENTS.md
|
||||
- CLAUDE.md
|
||||
|
||||
# Delimiters for the managed Spec Kit section
|
||||
context_markers:
|
||||
start: "<!-- SPECKIT START -->"
|
||||
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_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.
|
||||
|
||||
## Requirements
|
||||
|
||||
The bundled update scripts require **Python 3** with **PyYAML** for YAML/upsert processing (PowerShell can also use `ConvertFrom-Yaml` when available).
|
||||
|
||||
PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports _"PyYAML is required … not available in the current Python environment"_, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run:
|
||||
PyYAML ships with the `specify` CLI and is normally available via the same `python3` interpreter. If a hook reports *"PyYAML is required … not available in the current Python environment"*, it means the system `python3` differs from the one used to install Spec Kit. To resolve, run:
|
||||
|
||||
```bash
|
||||
pip install pyyaml
|
||||
@@ -58,6 +58,10 @@ pip install pyyaml
|
||||
/path/to/speckit-python -m pip install pyyaml
|
||||
```
|
||||
|
||||
## Issues
|
||||
## Disable
|
||||
|
||||
For any other issues, please create an issue in the [official GitHub repo](https://github.com/github/spec-kit/issues).
|
||||
```bash
|
||||
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.
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
# Coding Agent Context Extension Configuration
|
||||
# These values are populated automatically by `specify init` and
|
||||
# `specify integration use` / `specify integration install`.
|
||||
|
||||
# WHAT: The single agent context file relative to the project root (the directory containing .specify/). Absolute paths, backslash separators, and `..` path segments are rejected.
|
||||
# REQUIREMENT: OPTIONAL. Use this if you want to manually specify a single context file. If you leave this entry blank, it will use the default context file for the coding agent you picked when you set up Spec Kit. See `agent-context-defaults.json` for the defaults.
|
||||
# EXAMPLE: context_file: CLAUDE.md
|
||||
# Path (relative to the project root) to the default coding agent context file
|
||||
# managed by this extension (e.g. CLAUDE.md, AGENTS.md,
|
||||
# .github/copilot-instructions.md). Set automatically from the active
|
||||
# integration and regenerated during `specify init` or integration switches.
|
||||
context_file: ""
|
||||
|
||||
# WHAT: List of agent context files relative to the project root (the directory containing .specify/). If you have both `context_file` and `context_files` filled, then this (`context_files`) takes precedence. Absolute paths, backslash separators, and `..` path segments are rejected.
|
||||
# REQUIREMENT: OPTIONAL. Use this if your project requires you to keep multiple agent context files in sync.
|
||||
# EXAMPLE:
|
||||
# context_files:
|
||||
# - AGENTS.md
|
||||
# - CLAUDE.md
|
||||
# Optional list of project-relative coding agent context files managed by this
|
||||
# extension. When non-empty, this list takes precedence over `context_file`.
|
||||
# Use this for projects that intentionally keep multiple agent anchors in sync.
|
||||
context_files: []
|
||||
|
||||
# WHAT: Markers (delimiters) for the managed Spec Kit section. This extension injects information only between these markers.
|
||||
# REQUIREMENT: OPTIONAL. Only change if you wish to have a custom marker name.
|
||||
# EXAMPLE:
|
||||
# context_markers:
|
||||
# start: "<!-- AGENT SPEC KIT CONTEXT START -->"
|
||||
# end: "<!-- AGENT SPEC KIT CONTEXT END -->"
|
||||
# Delimiters for the managed Spec Kit section.
|
||||
# Edit these to use custom markers.
|
||||
context_markers:
|
||||
start: "<!-- SPECKIT START -->"
|
||||
end: "<!-- SPECKIT END -->"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"_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",
|
||||
"alquimia": "ALQUIMIA.md",
|
||||
"amp": "AGENTS.md",
|
||||
"auggie": ".augment/rules/specify-rules.md",
|
||||
"bob": "AGENTS.md",
|
||||
|
||||
@@ -176,18 +176,13 @@ _opts_lines=()
|
||||
while IFS= read -r _line || [[ -n "$_line" ]]; do
|
||||
_opts_lines+=("$_line")
|
||||
done < <(printf '%s\n' "$_raw_opts")
|
||||
if (( ${#_opts_lines[@]} < 1 )); then
|
||||
echo "agent-context: malformed config parser output; expected at least the context_files line, got ${#_opts_lines[@]}; skipping update." >&2
|
||||
if (( ${#_opts_lines[@]} < 3 )); then
|
||||
echo "agent-context: malformed config parser output; expected 3 lines (context_files, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
|
||||
exit 0
|
||||
fi
|
||||
# The marker lines may be absent: the $(...) capture above strips trailing
|
||||
# newlines, so blank markers (the config omitting context_markers and relying on
|
||||
# defaults) collapse the 3-line output to fewer lines. Default them to empty here
|
||||
# and let the DEFAULT_START/END substitution below fill them in, matching the
|
||||
# Python and PowerShell ports.
|
||||
CONTEXT_FILES_JSON="${_opts_lines[0]}"
|
||||
MARKER_START="${_opts_lines[1]:-}"
|
||||
MARKER_END="${_opts_lines[2]:-}"
|
||||
MARKER_START="${_opts_lines[1]}"
|
||||
MARKER_END="${_opts_lines[2]}"
|
||||
|
||||
if ! _context_files_raw="$("$_python" - "$CONTEXT_FILES_JSON" <<'PY'
|
||||
import json
|
||||
|
||||
@@ -11,9 +11,8 @@ Usage: update_agent_context.py [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 ``plan.md`` anywhere under ``specs/`` (including nested scoped
|
||||
layouts such as ``specs/<scope>/<feature>/plan.md``) only when feature.json is
|
||||
absent or its plan does not exist yet.
|
||||
recently modified ``specs/*/plan.md`` only when feature.json is absent or its
|
||||
plan does not exist yet.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -174,7 +173,7 @@ def _resolve_plan_path(project_root: str) -> str:
|
||||
if not plan_path:
|
||||
root = Path(project_root).resolve()
|
||||
plans = sorted(
|
||||
(root / "specs").rglob("plan.md"),
|
||||
(root / "specs").glob("*/plan.md"),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,6 @@ Discovery answers *"is this worth building?"* Delivery answers *"how do we build
|
||||
|
||||
## Overview
|
||||
|
||||
`assess` runs inside an initialized Spec Kit project (it writes assessments under `.specify/assessments/`), but that project can be **completely empty of source code** — a freshly initialized project with no code works just as well as an established codebase. The input is just an idea: pasted text, a URL, or a ticket need no existing code, while a codebase pointer lets you assess an idea for code that already exists. Neither starting point is more "correct" than the other.
|
||||
|
||||
Each idea lives in its own directory under `.specify/assessments/<slug>/`, with one Markdown artifact per stage:
|
||||
|
||||
```
|
||||
|
||||
@@ -21,8 +21,6 @@ The user input is the idea and (optionally) a slug. Treat it as one of:
|
||||
3. **A codebase pointer** — phrasing like "an idea for this repo" or a path. Read enough of the repository to record what the idea relates to.
|
||||
4. **A mix** of the above.
|
||||
|
||||
There is **no requirement for existing source code**: within an initialized Spec Kit project, intake works just as well when the project is empty of code as when it already has a codebase. Pasted text or a URL (options 1–2) need no existing codebase; a codebase pointer (option 3) targets existing code. Both are equally valid.
|
||||
|
||||
If the input is empty, ask the user for the idea (interactive), or stop with a note that there is nothing to intake (automated).
|
||||
|
||||
## Slug Resolution
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-07-29T00:00:00Z",
|
||||
"updated_at": "2026-07-21T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||
"extensions": {
|
||||
"aide": {
|
||||
@@ -290,8 +290,8 @@
|
||||
"id": "architecture-guard",
|
||||
"description": "Framework-agnostic architecture review extension for validating implementation against governance and architecture constitutions, detecting architectural drift, and generating non-blocking refactor tasks.",
|
||||
"author": "DyanGalih",
|
||||
"version": "1.13.1",
|
||||
"download_url": "https://github.com/DyanGalih/spec-kit-architecture-guard/archive/refs/tags/v1.13.1.zip",
|
||||
"version": "1.8.17",
|
||||
"download_url": "https://github.com/DyanGalih/spec-kit-architecture-guard/archive/refs/tags/v1.8.17.zip",
|
||||
"repository": "https://github.com/DyanGalih/spec-kit-architecture-guard",
|
||||
"homepage": "https://github.com/DyanGalih/spec-kit-architecture-guard",
|
||||
"documentation": "https://github.com/DyanGalih/spec-kit-architecture-guard/blob/main/docs/architecture-overview.md",
|
||||
@@ -303,7 +303,7 @@
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 14,
|
||||
"commands": 10,
|
||||
"hooks": 3
|
||||
},
|
||||
"tags": [
|
||||
@@ -313,14 +313,13 @@
|
||||
"refactor",
|
||||
"workflow",
|
||||
"governance",
|
||||
"guardrails",
|
||||
"hygiene"
|
||||
"guardrails"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-05T07:26:00Z",
|
||||
"updated_at": "2026-07-24T00:00:00Z"
|
||||
"updated_at": "2026-06-08T00:00:00Z"
|
||||
},
|
||||
"archive": {
|
||||
"name": "Archive Extension",
|
||||
@@ -490,46 +489,6 @@
|
||||
"created_at": "2026-04-17T00:00:00Z",
|
||||
"updated_at": "2026-04-17T00:00:00Z"
|
||||
},
|
||||
"blueprint-index": {
|
||||
"name": "Blueprint Index — Living Architecture Map",
|
||||
"id": "blueprint-index",
|
||||
"description": "Living architecture map for brownfield and greenfield projects, with a deterministic CI gate that blocks contradictions between the map, specs, and code while warning on non-blocking drift.",
|
||||
"author": "ogil109",
|
||||
"version": "0.2.0",
|
||||
"download_url": "https://github.com/ogil109/spec-kit-blueprint/releases/download/v0.2.0/blueprint.zip",
|
||||
"repository": "https://github.com/ogil109/spec-kit-blueprint",
|
||||
"homepage": "https://github.com/ogil109/spec-kit-blueprint/tree/main",
|
||||
"documentation": "https://github.com/ogil109/spec-kit-blueprint/blob/main/README.md",
|
||||
"changelog": "https://github.com/ogil109/spec-kit-blueprint/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.10.0",
|
||||
"tools": [
|
||||
{ "name": "bash", "required": false },
|
||||
{ "name": "git", "required": false }
|
||||
]
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"blueprint",
|
||||
"architecture",
|
||||
"coherence",
|
||||
"drift",
|
||||
"brownfield",
|
||||
"autonomous",
|
||||
"ci"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-24T00:00:00Z",
|
||||
"updated_at": "2026-07-24T00:00:00Z"
|
||||
},
|
||||
"branch-convention": {
|
||||
"name": "Branch Convention",
|
||||
"id": "branch-convention",
|
||||
@@ -1070,41 +1029,6 @@
|
||||
"created_at": "2026-03-29T00:00:00Z",
|
||||
"updated_at": "2026-03-29T00:00:00Z"
|
||||
},
|
||||
"contextforge-mcp": {
|
||||
"name": "ContextForge MCP",
|
||||
"id": "contextforge-mcp",
|
||||
"description": "Integrates codebase-memory-mcp + headroom into Spec Kit — graph-based code intelligence and context compression for the implement phase.",
|
||||
"author": "capatinore",
|
||||
"version": "0.1.0",
|
||||
"download_url": "https://github.com/capatinore/contextforge-mcp/releases/download/ext-v0.1.0/contextforge-mcp-speckit-extension.zip",
|
||||
"repository": "https://github.com/capatinore/contextforge-mcp",
|
||||
"homepage": "https://github.com/capatinore/contextforge-mcp",
|
||||
"documentation": "https://github.com/capatinore/contextforge-mcp/blob/main/README.md",
|
||||
"changelog": "",
|
||||
"license": "MIT",
|
||||
"category": "code",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.10.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"mcp",
|
||||
"code-intelligence",
|
||||
"context-compression",
|
||||
"tokens",
|
||||
"claude",
|
||||
"spec-driven-development"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-13T00:00:00Z",
|
||||
"updated_at": "2026-07-13T00:00:00Z"
|
||||
},
|
||||
"cost": {
|
||||
"name": "Cost Tracker",
|
||||
"id": "cost",
|
||||
@@ -1654,8 +1578,8 @@
|
||||
"id": "gates",
|
||||
"description": "Deterministic quality enforcement for Spec Kit across agent hooks, git checks, and CI pipelines with one policy file and one verify entrypoint for identical results at every boundary.",
|
||||
"author": "schwichtgit",
|
||||
"version": "0.3.3",
|
||||
"download_url": "https://github.com/schwichtgit/spec-gates/releases/download/v0.3.3/gates-0.3.3.zip",
|
||||
"version": "0.3.2",
|
||||
"download_url": "https://github.com/schwichtgit/spec-gates/releases/download/v0.3.2/gates-0.3.2.zip",
|
||||
"repository": "https://github.com/schwichtgit/spec-gates",
|
||||
"homepage": "https://github.com/schwichtgit/spec-gates",
|
||||
"documentation": "https://github.com/schwichtgit/spec-gates/blob/main/docs/how-it-works.md",
|
||||
@@ -1699,7 +1623,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-09T00:00:00Z",
|
||||
"updated_at": "2026-07-27T00:00:00Z"
|
||||
"updated_at": "2026-07-15T00:00:00Z"
|
||||
},
|
||||
"github-issues": {
|
||||
"name": "GitHub Issues Integration 1",
|
||||
@@ -1896,40 +1820,6 @@
|
||||
"created_at": "2026-06-23T00:00:00Z",
|
||||
"updated_at": "2026-06-30T00:00:00Z"
|
||||
},
|
||||
"intent": {
|
||||
"name": "Intent Reconciliation",
|
||||
"id": "intent",
|
||||
"description": "Reconcile implementation-discovered decisions against approved feature intent",
|
||||
"author": "SuhaibAslam",
|
||||
"version": "1.0.2",
|
||||
"download_url": "https://github.com/SuhaibAslam/spec-kit-reconcile/archive/refs/tags/v1.0.2.zip",
|
||||
"repository": "https://github.com/SuhaibAslam/spec-kit-reconcile",
|
||||
"homepage": "https://github.com/SuhaibAslam/spec-kit-reconcile",
|
||||
"documentation": "https://github.com/SuhaibAslam/spec-kit-reconcile/blob/main/README.md",
|
||||
"changelog": "https://github.com/SuhaibAslam/spec-kit-reconcile/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.12.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"intent",
|
||||
"decisions",
|
||||
"reconciliation",
|
||||
"drift",
|
||||
"workflow"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-29T00:00:00Z",
|
||||
"updated_at": "2026-07-29T00:00:00Z"
|
||||
},
|
||||
"issue": {
|
||||
"name": "GitHub Issues Integration 2",
|
||||
"id": "issue",
|
||||
@@ -2144,11 +2034,11 @@
|
||||
"id": "linear-weave",
|
||||
"description": "Weave Spec Kit into Linear: pull requirements, mirror tasks.md into sub-issues, sync statuses.",
|
||||
"author": "Tony Woodhouse",
|
||||
"version": "1.0.1",
|
||||
"download_url": "https://github.com/tonydwoodhouse/spec-kit-linear-weave/archive/refs/tags/v1.0.1.zip",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/tonydwoodhouse/spec-kit-linear-weave/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/tonydwoodhouse/spec-kit-linear-weave",
|
||||
"homepage": "https://github.com/tonydwoodhouse/spec-kit-linear-weave",
|
||||
"documentation": "https://github.com/tonydwoodhouse/spec-kit-linear-weave/blob/main/README.md",
|
||||
"documentation": "https://github.com/tonydwoodhouse/spec-kit-linear-weave#readme",
|
||||
"changelog": "https://github.com/tonydwoodhouse/spec-kit-linear-weave/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "integration",
|
||||
@@ -2171,7 +2061,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-21T00:00:00Z",
|
||||
"updated_at": "2026-07-27T00:00:00Z"
|
||||
"updated_at": "2026-07-21T00:00:00Z"
|
||||
},
|
||||
"loop": {
|
||||
"name": "Loop Engineering",
|
||||
@@ -4887,40 +4777,36 @@
|
||||
"verify-review-ship": {
|
||||
"name": "Verify Review Ship",
|
||||
"id": "verify-review-ship",
|
||||
"description": "Post-convergence operational verification, technical review, learning governance, and transactional delivery.",
|
||||
"description": "Adds post-implementation verify, review, and ship readiness gates to Spec Kit workflows.",
|
||||
"author": "Carlos Eduardo Gevaerd Araujo",
|
||||
"version": "0.4.2",
|
||||
"download_url": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/archive/refs/tags/v0.4.2.zip",
|
||||
"sha256": "71dceef5bf81d7ac54faa26bb5cf279554815a4928ee8d0c8e9bfb4c3e2bb0ab",
|
||||
"version": "0.1.0",
|
||||
"download_url": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/archive/refs/tags/v0.1.0.zip",
|
||||
"repository": "https://github.com/cadugevaerd/spec-kit-verify-review-ship",
|
||||
"homepage": "https://github.com/cadugevaerd/spec-kit-verify-review-ship",
|
||||
"documentation": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/blob/main/README.md",
|
||||
"changelog": "https://github.com/cadugevaerd/spec-kit-verify-review-ship/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"category": "process",
|
||||
"effect": "read-write",
|
||||
"effect": "read-only",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.11.2"
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 3,
|
||||
"hooks": 0
|
||||
"hooks": 1
|
||||
},
|
||||
"tags": [
|
||||
"quality",
|
||||
"review",
|
||||
"shipping",
|
||||
"merge",
|
||||
"cleanup",
|
||||
"learning",
|
||||
"governance",
|
||||
"agent-skills"
|
||||
"workflow",
|
||||
"testing"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-07-10T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
"updated_at": "2026-07-10T00:00:00Z"
|
||||
},
|
||||
"verify-tasks": {
|
||||
"name": "Verify Tasks Extension",
|
||||
|
||||
@@ -10,7 +10,7 @@ This extension provides Git operations as an optional, self-contained module. It
|
||||
- **Feature branch creation** with sequential (`001-feature-name`) or timestamp (`20260319-143022-feature-name`) numbering and optional templates for branch namespaces
|
||||
- **Branch validation** to ensure branches follow naming conventions
|
||||
- **Git remote detection** for GitHub integration (e.g., issue creation)
|
||||
- **Auto-commit** after core commands (configurable per-command with custom messages, or Conventional Commit messages generated by the agent)
|
||||
- **Auto-commit** after core commands (configurable per-command with custom messages)
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -66,11 +66,6 @@ branch_prefix: ""
|
||||
# Custom commit message for git init
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
|
||||
# Commit message style for auto-commit hooks: "fixed" (default) uses the
|
||||
# messages below; "conventional" asks the agent to generate a Conventional
|
||||
# Commit message (e.g. "feat: add OAuth spec") from the diff instead.
|
||||
commit_style: fixed
|
||||
|
||||
# Auto-commit per command (all disabled by default)
|
||||
# Example: enable auto-commit after specify
|
||||
auto_commit:
|
||||
|
||||
@@ -14,37 +14,23 @@ This command is invoked as a hook after (or before) core commands. It:
|
||||
2. Checks `.specify/extensions/git/git-config.yml` for the `auto_commit` section
|
||||
3. Looks up the specific event key to see if auto-commit is enabled
|
||||
4. Falls back to `auto_commit.default` if no event-specific key exists
|
||||
5. Determines the commit message based on `commit_style` (see below)
|
||||
5. Uses the per-command `message` if configured, otherwise a default message
|
||||
6. If enabled and there are uncommitted changes, runs `git add .` + `git commit`
|
||||
|
||||
## Commit Message Styles
|
||||
|
||||
Controlled by the `commit_style` key in `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
- **`fixed`** (default): use the per-command `message` if configured, otherwise a generic `[Spec Kit] Auto-commit <phase> <command>` message.
|
||||
- **`conventional`**: inspect the actual changes (`git diff` / `git status`) since the last commit and generate a single-line [Conventional Commit](https://www.conventionalcommits.org/) message (`type(scope): subject`, e.g. `feat: add OAuth specification` or `docs: update implementation plan`) that accurately summarizes the change. Write this message to a temporary file and pass the file's path to the script (see Execution below). The configured `message` values are ignored in this mode.
|
||||
|
||||
## Execution
|
||||
|
||||
Determine the event name from the hook that triggered this command, then run the script:
|
||||
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name> [--message-file <path>]`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name> [-MessageFile <path>]`
|
||||
- **Bash**: `.specify/extensions/git/scripts/bash/auto-commit.sh <event_name>`
|
||||
- **PowerShell**: `.specify/extensions/git/scripts/powershell/auto-commit.ps1 <event_name>`
|
||||
|
||||
Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`). Only pass a generated message when `commit_style: conventional` is configured — first check `.specify/extensions/git/git-config.yml` for the value of `commit_style`:
|
||||
|
||||
- If `conventional`: inspect the diff and generate a Conventional Commit message. **Do not interpolate the generated message directly into a shell command string** — its content is derived from repository changes and may contain characters (quotes, `$(...)`, backticks) that a shell would execute or that would break command quoting. Instead, write the message to a temporary file using your file-editing tool (not a shell `echo`/`printf`), then pass that file's path via `--message-file <path>` (Bash) or `-MessageFile <path>` (PowerShell).
|
||||
- If `fixed` or absent: run the script with just `<event_name>`; it uses the configured/static message.
|
||||
Replace `<event_name>` with the actual hook event (e.g., `after_specify`, `before_plan`, `after_implement`).
|
||||
|
||||
## Configuration
|
||||
|
||||
In `.specify/extensions/git/git-config.yml`:
|
||||
|
||||
```yaml
|
||||
# "fixed" (default) uses the messages below; "conventional" asks the agent
|
||||
# to generate a Conventional Commit message from the diff instead.
|
||||
commit_style: fixed
|
||||
|
||||
auto_commit:
|
||||
default: false # Global toggle — set true to enable for all commands
|
||||
after_specify:
|
||||
@@ -60,4 +46,3 @@ auto_commit:
|
||||
- If Git is not available or the current directory is not a repository: skips with a warning
|
||||
- If no config file exists: skips (disabled by default)
|
||||
- If no changes to commit: skips with a message
|
||||
- If `commit_style: conventional` is set and no generated message was supplied: fails with a clear error instead of silently falling back to the fixed message format
|
||||
|
||||
@@ -17,13 +17,6 @@ branch_prefix: ""
|
||||
# Commit message used by `git commit` during repository initialization
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
|
||||
# Commit message style used by auto-commit hooks (speckit.git.commit):
|
||||
# "fixed" - default; use the configured/static messages below.
|
||||
# "conventional" - ask the agent to inspect the diff and generate a
|
||||
# Conventional Commit message (e.g. "feat: add OAuth spec")
|
||||
# instead of using the messages configured below.
|
||||
commit_style: fixed
|
||||
|
||||
# Auto-commit before/after core commands.
|
||||
# Set "default" to enable for all commands, then override per-command.
|
||||
# Each key can be true/false. Message is customizable per-command.
|
||||
|
||||
@@ -17,13 +17,6 @@ branch_prefix: ""
|
||||
# Commit message used by `git commit` during repository initialization
|
||||
init_commit_message: "[Spec Kit] Initial commit"
|
||||
|
||||
# Commit message style used by auto-commit hooks (speckit.git.commit):
|
||||
# "fixed" - default; use the configured/static messages below.
|
||||
# "conventional" - ask the agent to inspect the diff and generate a
|
||||
# Conventional Commit message (e.g. "feat: add OAuth spec")
|
||||
# instead of using the messages configured below.
|
||||
commit_style: fixed
|
||||
|
||||
# Auto-commit before/after core commands.
|
||||
# Set "default" to enable for all commands, then override per-command.
|
||||
# Each key can be true/false. Message is customizable per-command.
|
||||
|
||||
@@ -3,57 +3,16 @@
|
||||
# Automatically commit changes after a Spec Kit command completes.
|
||||
# Checks per-command config keys in git-config.yml before committing.
|
||||
#
|
||||
# Usage: auto-commit.sh <event_name> [generated_message]
|
||||
# auto-commit.sh <event_name> --message-file <path>
|
||||
# Usage: auto-commit.sh <event_name>
|
||||
# e.g.: auto-commit.sh after_specify
|
||||
# e.g.: auto-commit.sh after_specify --message-file /tmp/commit-msg.txt (commit_style: conventional)
|
||||
#
|
||||
# --message-file is the preferred way to supply an agent-generated commit
|
||||
# message: it reads the message from a file instead of a shell argument,
|
||||
# so message content (which may contain quotes, `$(...)`, backticks, etc.)
|
||||
# is never interpolated into a shell command line.
|
||||
|
||||
set -e
|
||||
|
||||
EVENT_NAME="${1:-}"
|
||||
if [ -z "$EVENT_NAME" ]; then
|
||||
echo "Usage: $0 <event_name> [generated_message | --message-file <path>]" >&2
|
||||
echo "Usage: $0 <event_name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
shift || true
|
||||
|
||||
# Optional second argument: an agent-generated commit message (used when
|
||||
# commit_style: conventional is configured). Prefer --message-file over
|
||||
# passing the message directly as a shell argument.
|
||||
GENERATED_MESSAGE=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--message-file)
|
||||
_message_file="${2:-}"
|
||||
if [ -z "$_message_file" ]; then
|
||||
echo "[specify] Error: --message-file requires a path argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$_message_file" ]; then
|
||||
echo "[specify] Error: message file '$_message_file' not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
GENERATED_MESSAGE="$(cat "$_message_file")"
|
||||
# The message file is a transport-only artifact: its content is
|
||||
# now captured above, so remove it immediately. Otherwise, if it
|
||||
# was written inside the worktree, it would be picked up as an
|
||||
# untracked change by both the "any changes?" check below and by
|
||||
# `git add .`, polluting the commit or defeating the no-changes
|
||||
# short-circuit even when nothing else changed.
|
||||
rm -f "$_message_file"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
GENERATED_MESSAGE="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
@@ -87,22 +46,8 @@ fi
|
||||
_config_file="$REPO_ROOT/.specify/extensions/git/git-config.yml"
|
||||
_enabled=false
|
||||
_commit_msg=""
|
||||
_commit_style="fixed"
|
||||
|
||||
if [ -f "$_config_file" ]; then
|
||||
# Top-level scalar key: commit_style (fixed | conventional)
|
||||
_style_val=$(grep -m1 '^commit_style:' "$_config_file" 2>/dev/null | sed 's/^commit_style:[[:space:]]*//' | sed 's/[[:space:]]\{1,\}#.*$//' | sed 's/[[:space:]]*$//' | sed 's/^["'\'']//' | sed 's/["'\'']*$//' | tr '[:upper:]' '[:lower:]')
|
||||
if [ -n "$_style_val" ]; then
|
||||
case "$_style_val" in
|
||||
fixed|conventional)
|
||||
_commit_style="$_style_val"
|
||||
;;
|
||||
*)
|
||||
echo "[specify] Warning: unknown commit_style '$_style_val' in git-config.yml (expected 'fixed' or 'conventional'); defaulting to 'fixed'" >&2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Parse the auto_commit section for this event.
|
||||
# Look for auto_commit.<event_name>.enabled and .message
|
||||
# Also check auto_commit.default as fallback.
|
||||
@@ -149,12 +94,7 @@ if [ -f "$_config_file" ]; then
|
||||
[ "$_val" = "false" ] && _enabled=false
|
||||
fi
|
||||
if echo "$_line" | grep -Eq '[[:space:]]+message:'; then
|
||||
# Trim trailing whitespace before stripping the closing quote:
|
||||
# a value like `message: "Done" ` (trailing spaces after the
|
||||
# quote) would otherwise leave the quote dangling (`Done" `),
|
||||
# since the closing-quote strip is anchored to end-of-string.
|
||||
# The PowerShell twin .Trim()s first; match it for parity.
|
||||
_commit_msg=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | sed 's/[[:space:]]*$//' | sed 's/^["'\'']//' | sed 's/["'\'']*$//')
|
||||
_commit_msg=$(echo "$_line" | sed 's/^[^:]*:[[:space:]]*//' | sed 's/^["'\'']//' | sed 's/["'\'']*$//')
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -183,17 +123,6 @@ if git diff --quiet HEAD 2>/dev/null && git diff --cached --quiet 2>/dev/null &&
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# In conventional mode, the commit message must be supplied by the agent
|
||||
# (via the generated_message argument); never fall back to the fixed message.
|
||||
if [ "$_commit_style" = "conventional" ]; then
|
||||
if [ -n "$GENERATED_MESSAGE" ]; then
|
||||
_commit_msg="$GENERATED_MESSAGE"
|
||||
else
|
||||
echo "[specify] Error: commit_style is 'conventional' but no generated commit message was supplied; aborting auto-commit (pass --message-file <path>, or a raw message as arg 2, or set commit_style: fixed)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Derive a human-readable command name from the event
|
||||
# e.g., after_specify -> specify, before_plan -> plan
|
||||
_command_name=$(echo "$EVENT_NAME" | sed 's/^after_//' | sed 's/^before_//')
|
||||
|
||||
@@ -3,47 +3,14 @@
|
||||
# Automatically commit changes after a Spec Kit command completes.
|
||||
# Checks per-command config keys in git-config.yml before committing.
|
||||
#
|
||||
# Usage: auto-commit.ps1 <event_name> [generated_message]
|
||||
# auto-commit.ps1 <event_name> -MessageFile <path>
|
||||
# Usage: auto-commit.ps1 <event_name>
|
||||
# e.g.: auto-commit.ps1 after_specify
|
||||
# e.g.: auto-commit.ps1 after_specify -MessageFile C:\temp\commit-msg.txt (commit_style: conventional)
|
||||
#
|
||||
# -MessageFile is the preferred way to supply an agent-generated commit
|
||||
# message: it reads the message from a file instead of a shell argument,
|
||||
# so message content (which may contain quotes, $(...), backticks, etc.)
|
||||
# is never interpolated into a shell command line.
|
||||
param(
|
||||
[Parameter(Position = 0, Mandatory = $true)]
|
||||
[string]$EventName,
|
||||
|
||||
# Optional agent-generated commit message (used when commit_style: conventional is configured).
|
||||
# Prefer -MessageFile over passing the message directly as a shell argument.
|
||||
[Parameter(Position = 1, Mandatory = $false)]
|
||||
[string]$GeneratedMessage = "",
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$MessageFile = ""
|
||||
[string]$EventName
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($MessageFile) {
|
||||
if (-not (Test-Path $MessageFile -PathType Leaf)) {
|
||||
Write-Warning "[specify] Error: message file '$MessageFile' not found"
|
||||
exit 1
|
||||
}
|
||||
$GeneratedMessage = (Get-Content -Path $MessageFile -Raw)
|
||||
if ($null -ne $GeneratedMessage) {
|
||||
$GeneratedMessage = $GeneratedMessage.TrimEnd("`r", "`n")
|
||||
}
|
||||
# The message file is a transport-only artifact: its content is now
|
||||
# captured above, so remove it immediately. Otherwise, if it was written
|
||||
# inside the worktree, it would be picked up as an untracked change by
|
||||
# both the "any changes?" check below and by `git add .`, polluting the
|
||||
# commit or defeating the no-changes short-circuit even when nothing
|
||||
# else changed.
|
||||
Remove-Item -Path $MessageFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Find-ProjectRoot {
|
||||
param([string]$StartDir)
|
||||
$current = Resolve-Path $StartDir
|
||||
@@ -88,25 +55,8 @@ if (-not $isRepo) {
|
||||
$configFile = Join-Path $repoRoot ".specify/extensions/git/git-config.yml"
|
||||
$enabled = $false
|
||||
$commitMsg = ""
|
||||
$commitStyle = "fixed"
|
||||
|
||||
if (Test-Path $configFile) {
|
||||
# Top-level scalar key: commit_style (fixed | conventional)
|
||||
foreach ($line in Get-Content $configFile) {
|
||||
if ($line -match '^commit_style:\s*(.+)$') {
|
||||
$styleVal = (($matches[1] -replace '\s+#.*$', '').Trim()) -replace '^["'']' -replace '["'']$'
|
||||
if ($styleVal) {
|
||||
$styleVal = $styleVal.ToLower()
|
||||
if ($styleVal -eq 'fixed' -or $styleVal -eq 'conventional') {
|
||||
$commitStyle = $styleVal
|
||||
} else {
|
||||
Write-Warning "[specify] Warning: unknown commit_style '$styleVal' in git-config.yml (expected 'fixed' or 'conventional'); defaulting to 'fixed'"
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# Parse YAML to find auto_commit section
|
||||
$inAutoCommit = $false
|
||||
$inEvent = $false
|
||||
@@ -190,17 +140,6 @@ if ($d1 -eq 0 -and $d2 -eq 0 -and -not $untracked) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
# In conventional mode, the commit message must be supplied by the agent
|
||||
# (via the GeneratedMessage argument); never fall back to the fixed message.
|
||||
if ($commitStyle -eq 'conventional') {
|
||||
if ($GeneratedMessage) {
|
||||
$commitMsg = $GeneratedMessage
|
||||
} else {
|
||||
Write-Warning "[specify] Error: commit_style is 'conventional' but no generated commit message was supplied; aborting auto-commit (pass -MessageFile <path>, or a raw message as arg 2, or set commit_style: fixed)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Derive a human-readable command name from the event
|
||||
$commandName = $EventName -replace '^after_', '' -replace '^before_', ''
|
||||
$phase = if ($EventName -match '^before_') { 'before' } else { 'after' }
|
||||
|
||||
@@ -565,12 +565,6 @@ if (-not $DryRun) {
|
||||
$env:SPECIFY_FEATURE = $branchName
|
||||
}
|
||||
|
||||
# Build the PowerShell-idiomatic persist hint, mirroring the core
|
||||
# create-new-feature.ps1 twin (and the bash/python twins of this script), which
|
||||
# all emit "# To persist in your shell: ...".
|
||||
$quotedBranchName = "'" + $branchName.Replace("'", "''") + "'"
|
||||
$featureAssignment = '$env:SPECIFY_FEATURE = ' + $quotedBranchName
|
||||
|
||||
if ($Json) {
|
||||
$obj = [PSCustomObject]@{
|
||||
BRANCH_NAME = $branchName
|
||||
@@ -587,6 +581,6 @@ if ($Json) {
|
||||
Write-Output "BRANCH_NAME: $branchName"
|
||||
Write-Output "FEATURE_NUM: $featureNum"
|
||||
if (-not $DryRun) {
|
||||
Write-Output "# To persist in your shell: $featureAssignment"
|
||||
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,15 +33,7 @@ def _value_after_colon(line: str) -> str:
|
||||
|
||||
|
||||
def _strip_quotes(value: str) -> str:
|
||||
"""Strip surrounding whitespace, then one leading quote and all trailing quotes.
|
||||
|
||||
Trimming first matters when the YAML value has trailing whitespace after a
|
||||
closing quote (``message: "Done" ``): stripping quotes anchored to the end
|
||||
of string would leave the closing quote dangling (``Done" ``) because the
|
||||
quote is no longer at the end. The PowerShell twin ``.Trim()``s before
|
||||
stripping, so trim here too to keep all three script variants in parity.
|
||||
"""
|
||||
value = value.strip()
|
||||
"""Strip one leading quote and all trailing quotes, mirroring the bash sed."""
|
||||
value = re.sub(r"^[\"']", "", value)
|
||||
return re.sub(r"[\"']*$", "", value)
|
||||
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-07-27T00:00:00Z",
|
||||
"updated_at": "2026-07-15T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
|
||||
"integrations": {
|
||||
"alquimia": {
|
||||
"id": "alquimia",
|
||||
"name": "Alquimia AI",
|
||||
"version": "1.0.0",
|
||||
"description": "Alquimia AI CLI integration",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["alquimia"]
|
||||
},
|
||||
"claude": {
|
||||
"id": "claude",
|
||||
"name": "Claude Code",
|
||||
@@ -57,15 +48,6 @@
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["ide"]
|
||||
},
|
||||
"droid": {
|
||||
"id": "droid",
|
||||
"name": "Factory Droid",
|
||||
"version": "1.0.0",
|
||||
"description": "Factory Droid CLI skills-based integration",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"tags": ["cli", "skills", "factory"]
|
||||
},
|
||||
"amp": {
|
||||
"id": "amp",
|
||||
"name": "Amp",
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-07-28T00:00:00Z",
|
||||
|
||||
"updated_at": "2026-07-22T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
|
||||
"presets": {
|
||||
"a11y-governance": {
|
||||
"name": "A11Y Governance",
|
||||
"id": "a11y-governance",
|
||||
"version": "0.4.2",
|
||||
"description": "Adds accessibility (WCAG 2.2 AA), accessible text and JSON status parity, bilingual DE/EN delivery, CEFR-B2 readability, inclusive-content governance, didactic inline-code-comment review, and audit-ready Spec-Kit run evidence to Spec Kit.",
|
||||
"version": "0.4.0",
|
||||
"description": "Adds accessibility (WCAG 2.2 AA), bilingual DE/EN delivery, CEFR-B2 readability, inclusive-content governance, didactic inline-code-comment review, and audit-ready Spec Kit run evidence.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-a11y-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-a11y-governance/archive/refs/tags/v0.4.2.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-a11y-governance/archive/refs/tags/v0.4.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-a11y-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-a11y-governance/blob/v0.4.2/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-a11y-governance/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0"
|
||||
@@ -34,18 +33,18 @@
|
||||
"didactic-comments"
|
||||
],
|
||||
"created_at": "2026-04-27T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
"updated_at": "2026-06-14T00:00:00Z"
|
||||
},
|
||||
"agent-parity-governance": {
|
||||
"name": "Agent Parity Governance",
|
||||
"id": "agent-parity-governance",
|
||||
"version": "0.4.1",
|
||||
"description": "Adds shared-guidance and generated-command parity, repository-fleet completion evidence, secret-free runner/status metadata, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across declared AI-agent surfaces.",
|
||||
"version": "0.3.0",
|
||||
"description": "Adds shared-guidance parity, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across a project's declared AI-agent instruction surfaces so agent guidance does not drift.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/archive/refs/tags/v0.4.1.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/archive/refs/tags/v0.3.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/blob/v0.4.1/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0"
|
||||
@@ -64,7 +63,7 @@
|
||||
"multi-agent"
|
||||
],
|
||||
"created_at": "2026-04-27T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
"updated_at": "2026-06-14T00:00:00Z"
|
||||
},
|
||||
"aide-in-place": {
|
||||
"name": "AIDE In-Place Migration",
|
||||
@@ -97,13 +96,13 @@
|
||||
"architecture-governance": {
|
||||
"name": "Architecture Governance",
|
||||
"id": "architecture-governance",
|
||||
"version": "0.5.1",
|
||||
"description": "Adds secure software architecture, resumable remote-transaction boundaries, STRIDE+CAPEC threat modeling, arc42 security cross-cutting concepts, S-ADRs, Zero Trust applicability, OWASP SAMM governance, BSI C3A cloud autonomy, BSI C5 cloud compliance assurance, and audit-ready Spec Kit run evidence.",
|
||||
"version": "0.5.0",
|
||||
"description": "Adds secure software architecture, STRIDE+CAPEC threat modeling, arc42 security cross-cutting concepts, S-ADRs, Zero Trust applicability, OWASP SAMM governance, BSI C3A cloud autonomy, BSI C5 cloud compliance assurance, and audit-ready Spec Kit run evidence.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-architecture-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-architecture-governance/archive/refs/tags/v0.5.1.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-architecture-governance/archive/refs/tags/v0.5.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-architecture-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-architecture-governance/blob/v0.5.1/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-architecture-governance/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0"
|
||||
@@ -130,18 +129,18 @@
|
||||
"assurance"
|
||||
],
|
||||
"created_at": "2026-04-27T00:00:00Z",
|
||||
"updated_at": "2026-07-23T00:00:00Z"
|
||||
"updated_at": "2026-06-14T00:00:00Z"
|
||||
},
|
||||
"autonomous-run-governance": {
|
||||
"name": "Autonomous Run Governance",
|
||||
"id": "autonomous-run-governance",
|
||||
"version": "0.3.3",
|
||||
"description": "Adds permission-bounded autonomous delivery, an optional intake-review gate, and preservation of the project's learner and accessibility contract.",
|
||||
"version": "0.3.2",
|
||||
"description": "Adds permission-bounded, evidence-first governance for complete autonomous Spec Kit delivery, including validated status, stop, explicit resume, exact-head proof, post-merge closeout, retrospective learning, and an optional policy-driven intake-review gate before feature creation.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-autonomous-run-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-autonomous-run-governance/archive/refs/tags/v0.3.3.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-autonomous-run-governance/archive/refs/tags/v0.3.2.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-autonomous-run-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-autonomous-run-governance/blob/v0.3.3/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-autonomous-run-governance/blob/v0.3.2/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.3"
|
||||
@@ -156,10 +155,11 @@
|
||||
"governance",
|
||||
"evidence",
|
||||
"permissions",
|
||||
"accessibility"
|
||||
"resume",
|
||||
"intake-review"
|
||||
],
|
||||
"created_at": "2026-07-13T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
"updated_at": "2026-07-21T00:00:00Z"
|
||||
},
|
||||
"canon-core": {
|
||||
"name": "Canon Core",
|
||||
@@ -243,13 +243,13 @@
|
||||
"cross-platform-governance": {
|
||||
"name": "Cross-Platform Governance",
|
||||
"id": "cross-platform-governance",
|
||||
"version": "0.2.1",
|
||||
"description": "Adds Bash/PowerShell and read-only check parity, root-path and native-override review, Unix man pages, bilingual help, Verb-Noun discipline, and audit-ready evidence.",
|
||||
"version": "0.2.0",
|
||||
"description": "Adds Bash + PowerShell parity, Unix man-pages, bilingual comment-based help, Verb-Noun Cmdlet discipline, and audit-ready Spec Kit run evidence for scripting projects managed with Spec Kit.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/archive/refs/tags/v0.2.1.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/archive/refs/tags/v0.2.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/blob/v0.2.1/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0"
|
||||
@@ -271,7 +271,7 @@
|
||||
"linux"
|
||||
],
|
||||
"created_at": "2026-04-27T00:00:00Z",
|
||||
"updated_at": "2026-07-23T00:00:00Z"
|
||||
"updated_at": "2026-06-14T00:00:00Z"
|
||||
},
|
||||
"explicit-task-dependencies": {
|
||||
"name": "Explicit Task Dependencies",
|
||||
@@ -364,45 +364,16 @@
|
||||
"created_at": "2026-05-05T08:00:00Z",
|
||||
"updated_at": "2026-06-22T00:00:00Z"
|
||||
},
|
||||
"intake-authoring-governance": {
|
||||
"name": "Intake Authoring Governance",
|
||||
"id": "intake-authoring-governance",
|
||||
"version": "0.3.0",
|
||||
"description": "Governs traceable intake CRUD and language-aware requirements collections with atomic migrations, rollback evidence, and safe series authoring.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance/archive/refs/tags/v0.3.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-intake-authoring-governance/blob/v0.3.0/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.3"
|
||||
},
|
||||
"provides": {
|
||||
"templates": 12,
|
||||
"commands": 5,
|
||||
"scripts": 7
|
||||
},
|
||||
"tags": [
|
||||
"intake",
|
||||
"authoring",
|
||||
"governance",
|
||||
"requirements",
|
||||
"migration"
|
||||
],
|
||||
"created_at": "2026-07-22T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
},
|
||||
"intake-review-governance": {
|
||||
"name": "Intake Review Governance",
|
||||
"id": "intake-review-governance",
|
||||
"version": "0.2.0",
|
||||
"description": "Reviews single, series, campaign, and language-aware requirements collections before Spec Kit execution.",
|
||||
"version": "0.1.0",
|
||||
"description": "Adds hash-bound review, repair, and status gates for single, series, and campaign intake files before interactive, autonomous, or parallel Spec Kit execution.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-intake-review-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-intake-review-governance/archive/refs/tags/v0.2.0.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-intake-review-governance/archive/refs/tags/v0.1.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-intake-review-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-intake-review-governance/blob/v0.2.0/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-intake-review-governance/blob/v0.1.0/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.3"
|
||||
@@ -410,57 +381,28 @@
|
||||
"provides": {
|
||||
"templates": 8,
|
||||
"commands": 3,
|
||||
"scripts": 4
|
||||
"scripts": 2
|
||||
},
|
||||
"tags": [
|
||||
"intake",
|
||||
"review",
|
||||
"governance",
|
||||
"requirements",
|
||||
"quality-gate"
|
||||
"quality-gate",
|
||||
"autonomous"
|
||||
],
|
||||
"created_at": "2026-07-21T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
},
|
||||
"intake-sequencing-governance": {
|
||||
"name": "Intake Sequencing Governance",
|
||||
"id": "intake-sequencing-governance",
|
||||
"version": "0.2.2",
|
||||
"description": "Manages language-aware intake-series order, typed dependencies, lifecycle, and authority-neutral next-candidate selection.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance/archive/refs/tags/v0.2.2.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance/blob/v0.2.2/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.3"
|
||||
},
|
||||
"provides": {
|
||||
"templates": 11,
|
||||
"commands": 6,
|
||||
"scripts": 8
|
||||
},
|
||||
"tags": [
|
||||
"intake",
|
||||
"sequencing",
|
||||
"governance",
|
||||
"dag",
|
||||
"lifecycle"
|
||||
],
|
||||
"created_at": "2026-07-27T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
"updated_at": "2026-07-21T00:00:00Z"
|
||||
},
|
||||
"isaqb-architecture-governance": {
|
||||
"name": "iSAQB Architecture Governance",
|
||||
"id": "isaqb-architecture-governance",
|
||||
"version": "0.2.1",
|
||||
"description": "Adds iSAQB/CPSA-F and arc42 architecture governance with audit-ready evidence for goals, views, resumability, partial-failure scenarios, ADRs, risks, and technical debt.",
|
||||
"version": "0.2.0",
|
||||
"description": "Adds general iSAQB/CPSA-F and arc42 software-architecture governance, including audit-ready Spec Kit run evidence for architecture goals, views, quality scenarios, ADRs, risks, and technical debt.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/archive/refs/tags/v0.2.1.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/archive/refs/tags/v0.2.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/blob/v0.2.1/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0"
|
||||
@@ -481,7 +423,7 @@
|
||||
"technical-debt"
|
||||
],
|
||||
"created_at": "2026-04-27T00:00:00Z",
|
||||
"updated_at": "2026-07-23T00:00:00Z"
|
||||
"updated_at": "2026-06-14T00:00:00Z"
|
||||
},
|
||||
"jira": {
|
||||
"name": "Jira Issue Tracking",
|
||||
@@ -571,16 +513,16 @@
|
||||
"parallel-autonomous-run-governance": {
|
||||
"name": "Parallel Autonomous Run Governance",
|
||||
"id": "parallel-autonomous-run-governance",
|
||||
"version": "0.2.4",
|
||||
"description": "Coordinates permission-bounded autonomous campaigns while preserving the project's learner and accessibility contract across workers and consolidation.",
|
||||
"version": "0.2.3",
|
||||
"description": "Coordinates isolated autonomous Spec Kit campaigns with bounded concurrency, mixed agents, resumable consolidation, governed post-merge closeout, schema 1.2, and an optional current intake-review gate before worker scheduling.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance/archive/refs/tags/v0.2.4.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance/archive/refs/tags/v0.2.3.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance/blob/v0.2.4/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance/blob/v0.2.3/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.3"
|
||||
"speckit_version": ">=0.8.3"
|
||||
},
|
||||
"provides": {
|
||||
"templates": 9,
|
||||
@@ -591,11 +533,12 @@
|
||||
"parallel",
|
||||
"autonomous",
|
||||
"governance",
|
||||
"accessibility",
|
||||
"orchestration"
|
||||
"orchestration",
|
||||
"resume",
|
||||
"intake-review"
|
||||
],
|
||||
"created_at": "2026-07-22T00:00:00Z",
|
||||
"updated_at": "2026-07-28T00:00:00Z"
|
||||
"updated_at": "2026-07-22T00:00:00Z"
|
||||
},
|
||||
"pirate": {
|
||||
"name": "Pirate Speak (Full)",
|
||||
@@ -663,13 +606,13 @@
|
||||
"security-governance": {
|
||||
"name": "Security Governance",
|
||||
"id": "security-governance",
|
||||
"version": "0.6.1",
|
||||
"description": "Adds memory-safe-language and secure-coding governance, exact-head and security-gate evidence, provider-failure classification, ASVS, supply-chain transparency, and EU regulatory screening.",
|
||||
"version": "0.6.0",
|
||||
"description": "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 to Spec Kit.",
|
||||
"author": "Thorsten Hindermann",
|
||||
"repository": "https://github.com/hindermath/spec-kit-preset-security-governance",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.6.1.zip",
|
||||
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.6.0.zip",
|
||||
"homepage": "https://github.com/hindermath/spec-kit-preset-security-governance",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-security-governance/blob/v0.6.1/README.md",
|
||||
"documentation": "https://github.com/hindermath/spec-kit-preset-security-governance/blob/main/README.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.8.0"
|
||||
@@ -708,7 +651,7 @@
|
||||
"regulatory"
|
||||
],
|
||||
"created_at": "2026-04-27T00:00:00Z",
|
||||
"updated_at": "2026-07-23T00:00:00Z"
|
||||
"updated_at": "2026-06-14T00:00:00Z"
|
||||
},
|
||||
"sicario-core": {
|
||||
"name": "SicarioSpec Core",
|
||||
|
||||
@@ -25,29 +25,6 @@
|
||||
"workflow",
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"constitution-sync": {
|
||||
"name": "Constitution Template Sync",
|
||||
"id": "constitution-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Opt-in: restores /constitution propagation of amended guidance into plan/spec/tasks templates and installed command files, for teams that treat materialized templates as reviewed artifacts.",
|
||||
"author": "github",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"license": "MIT",
|
||||
"bundled": true,
|
||||
"requires": {
|
||||
"speckit_version": ">=0.14.4"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"templates": 0
|
||||
},
|
||||
"tags": [
|
||||
"constitution",
|
||||
"governance",
|
||||
"templates",
|
||||
"compatibility"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
# Constitution Template Sync
|
||||
|
||||
An **opt-in** preset that restores `/constitution`'s ability to propagate amended guidance into your
|
||||
project's own templates and command files. After you update the constitution, it aligns
|
||||
`plan-template.md`, `spec-template.md`, `tasks-template.md`, project-local command files, and
|
||||
guidance docs so they reflect the current principles.
|
||||
|
||||
This propagation used to be built into `/constitution`; it was dropped when the command moved to the
|
||||
preset model. Installing this preset opts you back into it: you get the guidance materialized into
|
||||
reviewed, committed artifacts instead of relying on runtime resolution alone.
|
||||
|
||||
> **What you're opting into.** Propagation was removed deliberately — it duplicates the constitution
|
||||
> as the source of truth and can fight the composition stack (materialized edits get shadowed or
|
||||
> clobbered on the next recompose). This preset knowingly **reintroduces** that behavior, and those
|
||||
> tradeoffs, for teams that want it. Read the [caveats](#caveats-you-take-on) before installing.
|
||||
|
||||
For most projects the default composable stack is the **recommended** approach, and at organization
|
||||
scale it is usually the stronger governance model. Runtime resolution keeps the live constitution as
|
||||
the single source of truth (nothing to re-sync, so nothing drifts), and the stack composes the
|
||||
**entire** Spec Kit ecosystem — not just the SDD commands, but every command, template, script and
|
||||
extension — with explicit priority levels, strategies, and independent versioning. It is a
|
||||
capability, not automatic governance: a core team authors its own organizational presets and
|
||||
extensions, then owns, versions, and audits that policy in one place and rolls it across many
|
||||
repositories, instead of scattering frozen, per-repo copies no central team can see. This preset is
|
||||
a supported escape hatch for teams whose workflow depends on reviewing materialized artifacts
|
||||
directly — useful as a bridge, though for org-wide policy the better long-term path is usually a
|
||||
versioned preset a core team maintains.
|
||||
|
||||
## What it does
|
||||
|
||||
Ships a single `wrap`-strategy override of `speckit.constitution`. It composes on top of the
|
||||
current core command (via `{CORE_TEMPLATE}`), so it stays forward-compatible with core changes, and
|
||||
appends a propagation pass that, after the constitution is written:
|
||||
|
||||
- Aligns `plan/spec/tasks-template.md` in `.specify/templates/` with the updated principles.
|
||||
- Updates **project-local** command files and guidance docs to correct stale references.
|
||||
- Extends the Sync Impact Report in `.specify/memory/constitution.md` with the files it touched.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- It does **not** change behavior for anyone who does not install it — the default runtime
|
||||
resolution model is untouched.
|
||||
- It does **not** disable runtime resolution. `plan`, `tasks`, and `analyze` still read the live
|
||||
constitution every run; this preset adds materialized copies on top — it does not replace the
|
||||
source of truth.
|
||||
- It does **not** edit versioned, package-owned files — templates or command files provided or
|
||||
wrapped by another preset or extension. Those are recomposed from the resolution stack, so it
|
||||
only ever writes into your project's own `.specify/templates/` scaffolds and command files that
|
||||
are not managed by a preset/extension.
|
||||
|
||||
## When to use it
|
||||
|
||||
Install it **only** if your team treats the materialized templates and commands as
|
||||
**reviewed, committed artifacts** — for example, if `plan-template.md`'s Constitution Check is
|
||||
read in PRs as "here are our current gates" and is expected to track the constitution.
|
||||
|
||||
If you rely on the default runtime-resolution model, you do **not** need this preset: the live
|
||||
constitution is already the single source of truth and there is nothing to sync.
|
||||
|
||||
## Caveats you take on
|
||||
|
||||
The preset resolution stack is how Spec Kit composes templates and commands going forward: they are
|
||||
**layered, package-owned artifacts recomposed on demand**, not frozen files you edit in place.
|
||||
Propagation is the opposite idea — it **materializes** guidance into files and freezes it. That
|
||||
tension is the main thing to understand before installing:
|
||||
|
||||
- **Materialized copies can drift.** Anything propagated is a snapshot; if you amend the
|
||||
constitution and do not re-run `/constitution`, the copies fall out of sync. The default runtime
|
||||
model has no drift because it reads the live constitution every run.
|
||||
|
||||
- **Edits to composed files do not survive reconciliation.** If the rest of your SDD flow is
|
||||
preset/extension-managed, the commands it materializes (`speckit.plan`, `speckit.specify`,
|
||||
`speckit.tasks`, `speckit.analyze`, `speckit.implement`, …) are recomputed from the stack. Any
|
||||
guidance propagated into them is clobbered the next time the stack reconciles — on
|
||||
`specify integration use <key>` / `switch`, `specify integration upgrade`, or any preset/extension
|
||||
install or remove. The same applies to templates owned by another preset/extension. This is why
|
||||
the preset restricts itself to project-local files; propagation is reliable **only** for
|
||||
artifacts you own outright.
|
||||
|
||||
- **A pre-filled Constitution Check can bias `/plan`.** Materializing concrete gates into
|
||||
`plan-template.md` replaces the runtime pointer, so the first `/plan` pass may anchor on the
|
||||
frozen text. Keep the pointer unless you specifically want committed gates.
|
||||
|
||||
**Bottom line:** this preset fits projects whose governed templates and commands are project-local
|
||||
artifacts they review, with the rest of the SDD flow on the plain bundled core. If your
|
||||
`plan`/`specify`/`tasks`/`analyze` commands or templates come from other presets or extensions,
|
||||
prefer the default runtime-resolution model.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# constitution-sync is a bundled preset — no download needed
|
||||
specify preset add constitution-sync
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Test from local directory
|
||||
specify preset add --dev ./presets/constitution-sync
|
||||
|
||||
# Verify the wrapped command resolves
|
||||
specify preset resolve speckit.constitution
|
||||
|
||||
# Remove when done
|
||||
specify preset remove constitution-sync
|
||||
```
|
||||
|
||||
## Migrating back to the default
|
||||
|
||||
To move back to runtime resolution, reset each materialized `## Constitution Check` section in
|
||||
`.specify/templates/plan-template.md` to the pointer:
|
||||
|
||||
```text
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
[Gates determined based on constitution file]
|
||||
```
|
||||
|
||||
Then remove this preset. See `docs/upgrade.md` for details.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
description: Create or update the project constitution, then propagate the amended guidance into dependent templates and installed command files (opt-in template sync).
|
||||
strategy: wrap
|
||||
handoffs:
|
||||
- label: Build Specification
|
||||
agent: speckit.specify
|
||||
prompt: Implement the feature specification based on the updated constitution. I want to build...
|
||||
---
|
||||
|
||||
{CORE_TEMPLATE}
|
||||
|
||||
## Constitution Template Sync
|
||||
|
||||
> **This section supersedes the "Scope Guard" above for template and command propagation.**
|
||||
> The core Scope Guard states that dependent templates and commands "are not modified here" —
|
||||
> that restriction is intentionally lifted by this preset. Propagating amended guidance into the
|
||||
> project's own dependent scaffolds and installed command files (as described below) **is in
|
||||
> scope** whenever this preset is installed. The Scope Guard's other constraints still apply:
|
||||
> do not implement features, generate application code, or touch artifacts unrelated to the
|
||||
> constitution/template workflow.
|
||||
|
||||
After you have written the updated constitution above, perform a consistency propagation pass
|
||||
so the dependent artifacts reflect the amended principles:
|
||||
|
||||
1. Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align
|
||||
with the updated principles. Only materialize concrete gate text here if your team intends to
|
||||
review it as committed content; otherwise leave the runtime pointer
|
||||
`[Gates determined based on constitution file]` in place so `/plan` fills it from the live
|
||||
constitution.
|
||||
2. Read `.specify/templates/spec-template.md` for scope/requirements alignment — update if the
|
||||
constitution adds/removes mandatory sections or constraints.
|
||||
3. Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or
|
||||
removed principle-driven task types (e.g., observability, versioning, testing discipline).
|
||||
4. Read each installed Spec Kit command file for your agent (including this one) — named
|
||||
`speckit.*` or `speckit-*` (dot or hyphen depending on the agent), or laid out as
|
||||
`speckit-<name>/SKILL.md` for skills-based integrations, e.g. in `.github/agents/`,
|
||||
`.github/skills/`, `.claude/skills/`, or your agent's equivalent commands directory — to verify
|
||||
no outdated references (CLAUDE-only or other agent-specific names) remain when generic guidance
|
||||
is required. **Only hand-edit a command file if it is a project-local file not managed by a
|
||||
preset or extension.** Command files that are composed from the resolution stack (anything
|
||||
provided or wrapped by a preset/extension) must be regenerated through the stack — do **not**
|
||||
edit them in place, because reconciliation (`specify integration use`, `specify integration
|
||||
upgrade`, or any preset/extension install/remove) will clobber the edits.
|
||||
5. Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific
|
||||
guidance files if present) and update references to principles that changed.
|
||||
|
||||
Then extend the Sync Impact Report at the top of `.specify/memory/constitution.md` with:
|
||||
|
||||
- Templates requiring updates (✅ updated / ⚠ pending) with file paths.
|
||||
|
||||
**Do not edit versioned preset- or extension-provided template or command files directly.** Those
|
||||
artifacts are owned by their packages and are recomposed on the package's next update or on stack
|
||||
reconciliation — hand edits are clobbered. Limit propagation to the project's own
|
||||
`.specify/templates/` scaffolds and to command files that are not managed by a preset or extension.
|
||||
@@ -1,30 +0,0 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
preset:
|
||||
id: "constitution-sync"
|
||||
name: "Constitution Template Sync"
|
||||
version: "1.0.0"
|
||||
description: "Opt-in: restores /constitution propagation of amended guidance into plan/spec/tasks templates and installed command files, for teams that treat materialized templates as reviewed artifacts."
|
||||
author: "github"
|
||||
repository: "https://github.com/github/spec-kit"
|
||||
license: "MIT"
|
||||
|
||||
requires:
|
||||
# Requires the runtime-resolution baseline (#3790, shipped in 0.14.4) where the
|
||||
# core /constitution command no longer propagates. Installing this preset on an
|
||||
# older core would double-apply propagation.
|
||||
speckit_version: ">=0.14.4"
|
||||
|
||||
provides:
|
||||
templates:
|
||||
- type: "command"
|
||||
name: "speckit.constitution"
|
||||
file: "commands/speckit.constitution.md"
|
||||
description: "Wrap /constitution to also propagate guidance into dependent templates and command files"
|
||||
strategy: "wrap"
|
||||
|
||||
tags:
|
||||
- "constitution"
|
||||
- "governance"
|
||||
- "templates"
|
||||
- "compatibility"
|
||||
@@ -8,24 +8,6 @@ description: Create or update the project constitution.
|
||||
$ARGUMENTS
|
||||
```
|
||||
|
||||
## Scope Guard
|
||||
|
||||
This command's own work is limited to creating or updating the project constitution and
|
||||
propagating constitution-driven changes to dependent Spec Kit artifacts.
|
||||
|
||||
- Classify every part of the user input as constitution content or a separate non-governance
|
||||
intent. Feature implementation, code generation, refactoring, build, and deployment requests
|
||||
are examples of non-governance intents.
|
||||
- You **MUST NOT** execute any non-governance intent. Defer each one to `Next Actions`.
|
||||
- You **MUST NOT** create, modify, or delete application source files or other artifacts
|
||||
unrelated to the constitution workflow.
|
||||
- If an instruction could be either constitution content or a non-governance intent, ask for
|
||||
clarification before making changes.
|
||||
- After updating the constitution, list each deferred intent in a `Next Actions` section with an
|
||||
appropriate follow-up Spec Kit command, such as `__SPECKIT_COMMAND_SPECIFY__`, but do not
|
||||
invoke it.
|
||||
- Omit `Next Actions` when there are no non-governance intents.
|
||||
|
||||
## Outline
|
||||
|
||||
1. Create or update the project constitution and store it in `.specify/memory/constitution.md`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.15.1"
|
||||
version = "0.13.3"
|
||||
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"
|
||||
@@ -39,7 +39,6 @@ packages = ["src/specify_cli"]
|
||||
"templates/commands" = "specify_cli/core_pack/commands"
|
||||
"scripts/bash" = "specify_cli/core_pack/scripts/bash"
|
||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
||||
"scripts/python" = "specify_cli/core_pack/scripts/python"
|
||||
# Bundled extensions (installable via `specify extension add <name>`)
|
||||
"extensions/git" = "specify_cli/core_pack/extensions/git"
|
||||
"extensions/agent-context" = "specify_cli/core_pack/extensions/agent-context"
|
||||
@@ -49,7 +48,6 @@ packages = ["src/specify_cli"]
|
||||
"workflows/speckit" = "specify_cli/core_pack/workflows/speckit"
|
||||
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
|
||||
"presets/lean" = "specify_cli/core_pack/presets/lean"
|
||||
"presets/constitution-sync" = "specify_cli/core_pack/presets/constitution-sync"
|
||||
# Community bundle catalog snapshot (used for offline discovery)
|
||||
"bundles/catalog.community.json" = "specify_cli/core_pack/bundles/catalog.community.json"
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ ALLOW_EXISTING=false
|
||||
SHORT_NAME=""
|
||||
BRANCH_NUMBER=""
|
||||
USE_TIMESTAMP=false
|
||||
NUMBER_EXPLICIT=false
|
||||
ARGS=()
|
||||
i=1
|
||||
while [ $i -le $# ]; do
|
||||
@@ -49,9 +48,6 @@ while [ $i -le $# ]; do
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER="$next_arg"
|
||||
if [ -n "$BRANCH_NUMBER" ]; then
|
||||
NUMBER_EXPLICIT=true
|
||||
fi
|
||||
;;
|
||||
--timestamp)
|
||||
USE_TIMESTAMP=true
|
||||
@@ -64,7 +60,7 @@ while [ $i -le $# ]; do
|
||||
echo " --dry-run Compute feature name and paths without creating directories or files"
|
||||
echo " --allow-existing-branch Reuse an existing feature directory if it already exists"
|
||||
echo " --short-name <name> Provide a custom short name (2-4 words) for the feature"
|
||||
echo " --number N Prefer a feature number (auto-corrected if its specs prefix exists)"
|
||||
echo " --number N Specify branch number manually (overrides auto-detection)"
|
||||
echo " --timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
||||
echo " --help, -h Show this help message"
|
||||
echo ""
|
||||
@@ -95,7 +91,6 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
|
||||
fi
|
||||
|
||||
MAX_FEATURE_NUMBER=9223372036854775807
|
||||
MAX_BRANCH_LENGTH=244
|
||||
|
||||
is_feature_number_in_range() {
|
||||
local value="$1"
|
||||
@@ -133,40 +128,12 @@ get_highest_from_specs() {
|
||||
echo "$highest"
|
||||
}
|
||||
|
||||
# Return success when a spec directory owns the given numeric prefix.
|
||||
spec_prefix_exists() {
|
||||
local specs_dir="$1"
|
||||
local feature_num="$2"
|
||||
|
||||
for spec_path in "$specs_dir/${feature_num}-"*; do
|
||||
[ -d "$spec_path" ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Function to clean and format a branch name
|
||||
clean_branch_name() {
|
||||
local name="$1"
|
||||
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
||||
}
|
||||
|
||||
# Fit a feature prefix and suffix within GitHub's branch-name limit.
|
||||
fit_branch_name() {
|
||||
local feature_num="$1"
|
||||
local branch_suffix="$2"
|
||||
local branch_name="${feature_num}-${branch_suffix}"
|
||||
|
||||
if [ ${#branch_name} -gt $MAX_BRANCH_LENGTH ]; then
|
||||
local prefix_length=$(( ${#feature_num} + 1 ))
|
||||
local max_suffix_length=$((MAX_BRANCH_LENGTH - prefix_length))
|
||||
local truncated_suffix
|
||||
truncated_suffix=$(printf '%s' "$branch_suffix" | cut -c "1-$max_suffix_length" | sed 's/-$//')
|
||||
branch_name="${feature_num}-${truncated_suffix}"
|
||||
fi
|
||||
|
||||
printf '%s' "$branch_name"
|
||||
}
|
||||
|
||||
# Quote a value for POSIX shell reuse, byte-identical to Python's shlex.quote
|
||||
# so the persistence hints match the Python variant exactly (printf %q output
|
||||
# differs between bash versions and from shlex.quote for spaces/metachars).
|
||||
@@ -286,41 +253,26 @@ else
|
||||
|
||||
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
|
||||
# Treat an explicit number as a preference when its prefix is already used
|
||||
# by a feature directory. Auto-detected numbers are already conflict-free.
|
||||
if [ "$NUMBER_EXPLICIT" = true ]; then
|
||||
SPEC_CONFLICT=false
|
||||
REQUESTED_BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
|
||||
REQUESTED_DIR="$SPECS_DIR/$REQUESTED_BRANCH_NAME"
|
||||
if [ "$ALLOW_EXISTING" != true ] || [ ! -d "$REQUESTED_DIR" ]; then
|
||||
spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" && SPEC_CONFLICT=true
|
||||
fi
|
||||
|
||||
if [ "$SPEC_CONFLICT" = true ]; then
|
||||
REQUESTED_NUM="$FEATURE_NUM"
|
||||
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
||||
BRANCH_NUMBER=$HIGHEST
|
||||
while true; do
|
||||
if [ "$BRANCH_NUMBER" -eq "$MAX_FEATURE_NUMBER" ]; then
|
||||
echo "Error: feature number must be between 0 and $MAX_FEATURE_NUMBER, got '9223372036854775808'" >&2
|
||||
exit 1
|
||||
fi
|
||||
BRANCH_NUMBER=$((BRANCH_NUMBER + 1))
|
||||
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
||||
spec_prefix_exists "$SPECS_DIR" "$FEATURE_NUM" || break
|
||||
done
|
||||
>&2 echo "[specify] Warning: --number $REQUESTED_NUM conflicts with an existing spec directory; using $FEATURE_NUM instead"
|
||||
fi
|
||||
fi
|
||||
|
||||
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
fi
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
# Validate and truncate if necessary
|
||||
ORIGINAL_BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
||||
BRANCH_NAME=$(fit_branch_name "$FEATURE_NUM" "$BRANCH_SUFFIX")
|
||||
if [ "$BRANCH_NAME" != "$ORIGINAL_BRANCH_NAME" ]; then
|
||||
MAX_BRANCH_LENGTH=244
|
||||
if [ ${#BRANCH_NAME} -gt $MAX_BRANCH_LENGTH ]; then
|
||||
# Calculate how much we need to trim from suffix
|
||||
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
|
||||
PREFIX_LENGTH=$(( ${#FEATURE_NUM} + 1 ))
|
||||
MAX_SUFFIX_LENGTH=$((MAX_BRANCH_LENGTH - PREFIX_LENGTH))
|
||||
|
||||
# Truncate suffix at word boundary if possible
|
||||
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
||||
# Remove trailing hyphen if truncation created one
|
||||
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
||||
|
||||
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
||||
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
||||
|
||||
>&2 echo "[specify] Warning: Branch name exceeded GitHub's 244-byte limit"
|
||||
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
||||
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
||||
|
||||
@@ -55,17 +55,9 @@ function Resolve-SpecifyInitDir {
|
||||
}
|
||||
# Resolve-Path echoes back any trailing separator from the input; trim it so
|
||||
# the returned root matches the bash resolver, whose `cd && pwd` never yields
|
||||
# one. TrimEnd (not [Path]::TrimEndingDirectorySeparator, which is .NET Core
|
||||
# only) keeps this working on Windows PowerShell 5.1 / .NET Framework, as
|
||||
# Get-FeaturePathsEnv already does below. Unlike a bare TrimEnd, the
|
||||
# GetPathRoot check preserves a path that *is* its own root ('C:\' must not
|
||||
# become 'C:', which every later API re-resolves against the current
|
||||
# directory instead of the drive root). No-op on a path with no trailing
|
||||
# separator.
|
||||
$initRoot = $resolved.Path.TrimEnd('/', '\')
|
||||
if ($initRoot.Length -lt [System.IO.Path]::GetPathRoot($resolved.Path).Length) {
|
||||
$initRoot = $resolved.Path
|
||||
}
|
||||
# one. TrimEndingDirectorySeparator is a no-op on a bare root and on a path
|
||||
# that already has no trailing separator.
|
||||
$initRoot = [System.IO.Path]::TrimEndingDirectorySeparator($resolved.Path)
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $initRoot '.specify') -PathType Container)) {
|
||||
[Console]::Error.WriteLine("ERROR: SPECIFY_INIT_DIR is not a Spec Kit project (no .specify/ directory): $initRoot")
|
||||
if ($ReturnNullOnError) { return $null }
|
||||
|
||||
@@ -14,7 +14,6 @@ param(
|
||||
[string[]]$FeatureDescription
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$maxBranchLength = 244
|
||||
|
||||
# Show help if requested
|
||||
if ($Help) {
|
||||
@@ -25,7 +24,7 @@ if ($Help) {
|
||||
Write-Host " -DryRun Compute feature name and paths without creating directories or files"
|
||||
Write-Host " -AllowExistingBranch Reuse an existing feature directory if it already exists"
|
||||
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the feature"
|
||||
Write-Host " -Number N Prefer a feature number (auto-corrected if its specs prefix exists)"
|
||||
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
||||
Write-Host " -Timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering"
|
||||
Write-Host " -Help Show this help message"
|
||||
Write-Host ""
|
||||
@@ -68,44 +67,11 @@ function Get-HighestNumberFromSpecs {
|
||||
return $highest
|
||||
}
|
||||
|
||||
function Test-SpecPrefixInUse {
|
||||
param(
|
||||
[string]$SpecsDir,
|
||||
[string]$FeatureNum
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SpecsDir -PathType Container)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
return $null -ne (Get-ChildItem -LiteralPath $SpecsDir -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "$FeatureNum-*" } |
|
||||
Select-Object -First 1)
|
||||
}
|
||||
|
||||
function ConvertTo-CleanBranchName {
|
||||
param([string]$Name)
|
||||
|
||||
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
||||
}
|
||||
|
||||
function Get-FittedBranchName {
|
||||
param(
|
||||
[string]$FeatureNum,
|
||||
[string]$BranchSuffix
|
||||
)
|
||||
|
||||
$fittedName = "$FeatureNum-$BranchSuffix"
|
||||
if ($fittedName.Length -gt $maxBranchLength) {
|
||||
$prefixLength = $FeatureNum.Length + 1
|
||||
$maxSuffixLength = $maxBranchLength - $prefixLength
|
||||
$truncatedSuffix = $BranchSuffix.Substring(0, [Math]::Min($BranchSuffix.Length, $maxSuffixLength))
|
||||
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
||||
$fittedName = "$FeatureNum-$truncatedSuffix"
|
||||
}
|
||||
|
||||
return $fittedName
|
||||
}
|
||||
# Load common functions (includes Get-RepoRoot and Resolve-Template)
|
||||
. "$PSScriptRoot/common.ps1"
|
||||
|
||||
@@ -210,40 +176,26 @@ if ($Timestamp) {
|
||||
}
|
||||
|
||||
$featureNum = ('{0:000}' -f $resolvedNumber)
|
||||
|
||||
# Treat an explicit number as a preference when its prefix is already used
|
||||
# by a feature directory. Auto-detected numbers are already conflict-free.
|
||||
$specConflict = $false
|
||||
if ($hasNumber -and (Test-Path -LiteralPath $specsDir -PathType Container)) {
|
||||
$requestedBranchName = Get-FittedBranchName -FeatureNum $featureNum -BranchSuffix $branchSuffix
|
||||
$requestedDir = Join-Path $specsDir $requestedBranchName
|
||||
if (-not $AllowExistingBranch -or -not (Test-Path -LiteralPath $requestedDir -PathType Container)) {
|
||||
$specConflict = Test-SpecPrefixInUse -SpecsDir $specsDir -FeatureNum $featureNum
|
||||
}
|
||||
}
|
||||
|
||||
if ($specConflict) {
|
||||
$requestedNum = $featureNum
|
||||
$highestNumber = Get-HighestNumberFromSpecs -SpecsDir $specsDir
|
||||
$resolvedNumber = $highestNumber
|
||||
do {
|
||||
if ($resolvedNumber -eq [long]::MaxValue) {
|
||||
Write-Error "Error: feature number must be between 0 and $([long]::MaxValue), got '9223372036854775808'"
|
||||
exit 1
|
||||
}
|
||||
$resolvedNumber++
|
||||
$featureNum = ('{0:000}' -f $resolvedNumber)
|
||||
} while (Test-SpecPrefixInUse -SpecsDir $specsDir -FeatureNum $featureNum)
|
||||
[Console]::Error.WriteLine("[specify] Warning: -Number $requestedNum conflicts with an existing spec directory; using $featureNum instead")
|
||||
}
|
||||
|
||||
$branchName = "$featureNum-$branchSuffix"
|
||||
}
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names
|
||||
# Validate and truncate if necessary
|
||||
$originalBranchName = "$featureNum-$branchSuffix"
|
||||
$branchName = Get-FittedBranchName -FeatureNum $featureNum -BranchSuffix $branchSuffix
|
||||
if ($branchName -ne $originalBranchName) {
|
||||
$maxBranchLength = 244
|
||||
if ($branchName.Length -gt $maxBranchLength) {
|
||||
# Calculate how much we need to trim from suffix
|
||||
# Account for prefix length: timestamp (15) + hyphen (1) = 16, or sequential (3) + hyphen (1) = 4
|
||||
$prefixLength = $featureNum.Length + 1
|
||||
$maxSuffixLength = $maxBranchLength - $prefixLength
|
||||
|
||||
# Truncate suffix
|
||||
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
||||
# Remove trailing hyphen if truncation created one
|
||||
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
||||
|
||||
$originalBranchName = $branchName
|
||||
$branchName = "$featureNum-$truncatedSuffix"
|
||||
|
||||
[Console]::Error.WriteLine("[specify] Warning: Branch name exceeded GitHub's 244-byte limit")
|
||||
[Console]::Error.WriteLine("[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)")
|
||||
[Console]::Error.WriteLine("[specify] Truncated to: $branchName ($($branchName.Length) bytes)")
|
||||
|
||||
@@ -258,27 +258,16 @@ def get_invoke_separator(repo_root: Path) -> str:
|
||||
integration_json = repo_root / ".specify" / "integration.json"
|
||||
if not integration_json.is_file():
|
||||
return "."
|
||||
# Split the parse out of the lookup and guard the top-level shape, matching
|
||||
# read_feature_json_feature_directory above and the bash/PowerShell twins,
|
||||
# which both fall back to "." for any unusable integration.json:
|
||||
# * a non-mapping top level ([], "forge", 42, null) is valid JSON, so
|
||||
# json.JSONDecodeError never fires and state.get(...) raised
|
||||
# AttributeError;
|
||||
# * a non-UTF-8 file raises UnicodeDecodeError, which is a ValueError --
|
||||
# not an OSError -- so it escaped the except tuple. Realistic on
|
||||
# Windows, where PowerShell 5.1's Out-File/`>` default to UTF-16.
|
||||
try:
|
||||
state = json.loads(integration_json.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||
return "."
|
||||
if not isinstance(state, dict):
|
||||
return "."
|
||||
key = state.get("default_integration") or state.get("integration") or ""
|
||||
settings = state.get("integration_settings")
|
||||
if isinstance(key, str) and isinstance(settings, dict):
|
||||
entry = settings.get(key)
|
||||
if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
|
||||
return entry["invoke_separator"]
|
||||
key = state.get("default_integration") or state.get("integration") or ""
|
||||
settings = state.get("integration_settings")
|
||||
if isinstance(key, str) and isinstance(settings, dict):
|
||||
entry = settings.get(key)
|
||||
if isinstance(entry, dict) and entry.get("invoke_separator") in {".", "-"}:
|
||||
return entry["invoke_separator"]
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
return "."
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ Options:
|
||||
--dry-run Compute feature name and paths without creating directories or files
|
||||
--allow-existing-branch Reuse an existing feature directory if it already exists
|
||||
--short-name <name> Provide a custom short name (2-4 words) for the feature
|
||||
--number N Prefer a feature number (auto-corrected if its specs prefix exists)
|
||||
--number N Specify branch number manually (overrides auto-detection)
|
||||
--timestamp Use timestamp prefix (YYYYMMDD-HHMMSS) instead of sequential numbering
|
||||
--help, -h Show this help message
|
||||
|
||||
@@ -204,43 +204,6 @@ def _get_highest_from_specs(specs_dir: Path) -> int:
|
||||
return highest
|
||||
|
||||
|
||||
def _fit_branch_name(feature_num: str, branch_suffix: str) -> str:
|
||||
"""Fit a feature prefix and suffix within GitHub's branch-name limit."""
|
||||
branch_name = f"{feature_num}-{branch_suffix}"
|
||||
if len(branch_name) <= _MAX_BRANCH_LENGTH:
|
||||
return branch_name
|
||||
|
||||
max_suffix_length = _MAX_BRANCH_LENGTH - (len(feature_num) + 1)
|
||||
truncated_suffix = re.sub(r"-$", "", branch_suffix[:max_suffix_length])
|
||||
return f"{feature_num}-{truncated_suffix}"
|
||||
|
||||
|
||||
def _spec_prefix_exists(specs_dir: Path, feature_num: str) -> bool:
|
||||
"""Return whether a spec directory owns the given numeric prefix."""
|
||||
try:
|
||||
return any(
|
||||
entry.is_dir() and entry.name.startswith(f"{feature_num}-")
|
||||
for entry in specs_dir.iterdir()
|
||||
)
|
||||
except OSError:
|
||||
# Match Bash globbing and PowerShell's ErrorAction=SilentlyContinue.
|
||||
return False
|
||||
|
||||
|
||||
def _has_spec_prefix_conflict(
|
||||
specs_dir: Path,
|
||||
feature_num: str,
|
||||
requested_dir: Path,
|
||||
*,
|
||||
allow_existing: bool,
|
||||
) -> bool:
|
||||
"""Return whether another spec directory owns the requested prefix."""
|
||||
if allow_existing and requested_dir.is_dir():
|
||||
return False
|
||||
|
||||
return _spec_prefix_exists(specs_dir, feature_num)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
argv0 = sys.argv[0]
|
||||
args = _parse_args(list(argv if argv is not None else sys.argv[1:]), argv0)
|
||||
@@ -298,48 +261,18 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 1
|
||||
feature_num = f"{number:03d}"
|
||||
|
||||
# Treat an explicit number as a preference when its prefix is already used
|
||||
# by a feature directory. Auto-detected numbers are already conflict-free.
|
||||
if branch_number:
|
||||
requested_branch_name = _fit_branch_name(feature_num, branch_suffix)
|
||||
requested_dir = specs_dir / requested_branch_name
|
||||
spec_conflict = _has_spec_prefix_conflict(
|
||||
specs_dir,
|
||||
feature_num,
|
||||
requested_dir,
|
||||
allow_existing=args.allow_existing,
|
||||
)
|
||||
if spec_conflict:
|
||||
requested_num = feature_num
|
||||
number = _get_highest_from_specs(specs_dir)
|
||||
while True:
|
||||
number += 1
|
||||
if number > _MAX_FEATURE_NUMBER:
|
||||
print(
|
||||
f"Error: feature number must be between 0 and "
|
||||
f"{_MAX_FEATURE_NUMBER}, got '{number}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
feature_num = f"{number:03d}"
|
||||
if not _spec_prefix_exists(specs_dir, feature_num):
|
||||
break
|
||||
print(
|
||||
f"[specify] Warning: --number {requested_num} conflicts with "
|
||||
f"an existing spec directory; using {feature_num} instead",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
max_suffix_length = _MAX_BRANCH_LENGTH - (len(feature_num) + 1)
|
||||
if max_suffix_length <= 0:
|
||||
print("Error: feature number is too long for a branch name", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
original_branch_name = f"{feature_num}-{branch_suffix}"
|
||||
branch_name = _fit_branch_name(feature_num, branch_suffix)
|
||||
branch_name = f"{feature_num}-{branch_suffix}"
|
||||
|
||||
# GitHub enforces a 244-byte limit on branch names.
|
||||
if branch_name != original_branch_name:
|
||||
if len(branch_name) > _MAX_BRANCH_LENGTH:
|
||||
truncated_suffix = re.sub(r"-$", "", branch_suffix[:max_suffix_length])
|
||||
original_branch_name = branch_name
|
||||
branch_name = f"{feature_num}-{truncated_suffix}"
|
||||
print(
|
||||
"[specify] Warning: Branch name exceeded GitHub's 244-byte limit",
|
||||
file=sys.stderr,
|
||||
|
||||
@@ -114,7 +114,6 @@ def _refresh_shared_templates(
|
||||
project_path: Path,
|
||||
*,
|
||||
invoke_separator: str,
|
||||
invoke_prefix: str = "/",
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Refresh default-sensitive shared templates without touching scripts."""
|
||||
@@ -125,7 +124,6 @@ def _refresh_shared_templates(
|
||||
repo_root=_repo_root(),
|
||||
console=console,
|
||||
invoke_separator=invoke_separator,
|
||||
invoke_prefix=invoke_prefix,
|
||||
force=force,
|
||||
)
|
||||
|
||||
@@ -136,7 +134,6 @@ def _install_shared_infra(
|
||||
tracker: StepTracker | None = None,
|
||||
force: bool = False,
|
||||
invoke_separator: str = ".",
|
||||
invoke_prefix: str = "/",
|
||||
refresh_managed: bool = False,
|
||||
refresh_hint: str | None = None,
|
||||
) -> bool:
|
||||
@@ -180,7 +177,6 @@ def _install_shared_infra(
|
||||
console=console,
|
||||
force=force,
|
||||
invoke_separator=invoke_separator,
|
||||
invoke_prefix=invoke_prefix,
|
||||
refresh_managed=refresh_managed,
|
||||
refresh_hint=refresh_hint,
|
||||
)
|
||||
@@ -192,7 +188,6 @@ def _install_shared_infra_or_exit(
|
||||
tracker: StepTracker | None = None,
|
||||
force: bool = False,
|
||||
invoke_separator: str = ".",
|
||||
invoke_prefix: str = "/",
|
||||
refresh_managed: bool = False,
|
||||
refresh_hint: str | None = None,
|
||||
) -> bool:
|
||||
@@ -203,7 +198,6 @@ def _install_shared_infra_or_exit(
|
||||
tracker=tracker,
|
||||
force=force,
|
||||
invoke_separator=invoke_separator,
|
||||
invoke_prefix=invoke_prefix,
|
||||
refresh_managed=refresh_managed,
|
||||
refresh_hint=refresh_hint,
|
||||
)
|
||||
@@ -514,11 +508,6 @@ _register_extension_cmds(app)
|
||||
from .integrations._commands import register as _register_integration_cmds # noqa: E402
|
||||
_register_integration_cmds(app)
|
||||
|
||||
|
||||
# ===== Event Commands =====
|
||||
from .commands.event import register as _register_event_cmds # noqa: E402
|
||||
_register_event_cmds(app)
|
||||
|
||||
# Re-export selected helpers to preserve the public import surface.
|
||||
from .integrations._helpers import ( # noqa: E402
|
||||
_clear_init_options_for_integration as _clear_init_options_for_integration,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,19 +100,8 @@ def resolve_github_release_asset_api_url(
|
||||
import json
|
||||
import urllib.error
|
||||
|
||||
from specify_cli._download_security import read_response_limited
|
||||
|
||||
# Accessing ``.hostname`` (like ``.port`` below) raises ValueError on a
|
||||
# malformed authority, e.g. an invalid bracketed IPv6 host
|
||||
# ``https://[not-an-ip]/...``. The function's contract is to return None for
|
||||
# anything it can't resolve, not to raise, so guard the read. ``download_url``
|
||||
# is server-controlled here (a catalog ``download_url`` payload), so a
|
||||
# malformed value must not leak a raw traceback past the caller.
|
||||
try:
|
||||
parsed = urlparse(download_url)
|
||||
hostname = (parsed.hostname or "").lower()
|
||||
except ValueError:
|
||||
return None
|
||||
parsed = urlparse(download_url)
|
||||
hostname = (parsed.hostname or "").lower()
|
||||
parts = [unquote(part) for part in parsed.path.strip("/").split("/")]
|
||||
|
||||
is_ghes = (
|
||||
@@ -159,9 +148,8 @@ def resolve_github_release_asset_api_url(
|
||||
if len(parts) < 6 or parts[2:4] != ["releases", "download"]:
|
||||
return None
|
||||
|
||||
owner, repo = parts[0], parts[1]
|
||||
tag = "/".join(parts[4:-1])
|
||||
asset_name = parts[-1]
|
||||
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}"
|
||||
|
||||
@@ -170,13 +158,10 @@ def resolve_github_release_asset_api_url(
|
||||
if redirect_validator is not None:
|
||||
open_kwargs["redirect_validator"] = redirect_validator
|
||||
with open_url_fn(release_url, **open_kwargs) as response:
|
||||
release_data = json.loads(
|
||||
read_response_limited(
|
||||
response,
|
||||
max_bytes=max_metadata_bytes,
|
||||
label=f"GitHub release metadata {release_url}",
|
||||
)
|
||||
)
|
||||
raw_release_data = response.read(max_metadata_bytes + 1)
|
||||
if len(raw_release_data) > max_metadata_bytes:
|
||||
raise ValueError("GitHub release metadata exceeds size limit")
|
||||
release_data = json.loads(raw_release_data)
|
||||
except (
|
||||
urllib.error.URLError,
|
||||
json.JSONDecodeError,
|
||||
|
||||
@@ -3,22 +3,12 @@
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any, Union
|
||||
from typing import Any
|
||||
|
||||
|
||||
INIT_OPTIONS_FILE = ".specify/init-options.json"
|
||||
|
||||
|
||||
class _MissingInitOptionsFile:
|
||||
"""Sentinel: init-options.json does not exist at all (legacy layout)."""
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover - debug aid only
|
||||
return "MISSING_INIT_OPTIONS_FILE"
|
||||
|
||||
|
||||
MISSING_INIT_OPTIONS_FILE = _MissingInitOptionsFile()
|
||||
|
||||
|
||||
def save_init_options(project_path: Path, options: dict[str, Any]) -> None:
|
||||
"""Persist the CLI options used during ``specify init``."""
|
||||
dest = project_path / INIT_OPTIONS_FILE
|
||||
@@ -44,40 +34,3 @@ def load_init_options(project_path: Path) -> dict[str, Any]:
|
||||
def is_ai_skills_enabled(opts: Mapping[str, Any] | None) -> bool:
|
||||
"""Return True only when init options explicitly enable AI skills."""
|
||||
return isinstance(opts, Mapping) and opts.get("ai_skills") is True
|
||||
|
||||
|
||||
def resolve_active_agent_for_registration(
|
||||
project_path: Path,
|
||||
) -> Union[str, None, _MissingInitOptionsFile]:
|
||||
"""Resolve the active integration key for active-only registration (#2948).
|
||||
|
||||
``load_init_options`` collapses "no file", "unreadable/malformed file",
|
||||
and "valid file with no recorded active agent" into the same ``{}``
|
||||
result, which previously made corrupted-but-present init-options behave
|
||||
like a legacy pre-init-options project and fall back to registering
|
||||
every detected agent. This helper distinguishes those cases explicitly:
|
||||
|
||||
- Returns :data:`MISSING_INIT_OPTIONS_FILE` when init-options.json does
|
||||
not exist at all (pre-init-options layout or direct library use).
|
||||
Callers should fall back to detection-based registration for all
|
||||
agents, matching the original pre-#2948 behavior for such projects.
|
||||
- Returns ``None`` when init-options.json exists but could not provide a
|
||||
valid non-empty string active agent (malformed/unreadable JSON,
|
||||
non-object payload, or a non-string/empty ``ai`` value). Callers must
|
||||
fail closed (register nothing) rather than treat this like "no file"
|
||||
or pass a non-string key into agent-config lookups.
|
||||
- Returns the active agent key (a non-empty string) otherwise.
|
||||
"""
|
||||
path = project_path / INIT_OPTIONS_FILE
|
||||
# A dangling symlink's target doesn't exist, so Path.exists() (which
|
||||
# follows symlinks) returns False even though the path itself is
|
||||
# present as a broken/corrupted entry. Treat any symlink as "present"
|
||||
# so a dangling one fails closed via the invalid-file branch below
|
||||
# instead of being mistaken for "no file at all" (legacy fallback).
|
||||
if not path.is_symlink() and not path.exists():
|
||||
return MISSING_INIT_OPTIONS_FILE
|
||||
|
||||
active_agent = load_init_options(project_path).get("ai")
|
||||
if isinstance(active_agent, str) and active_agent:
|
||||
return active_agent
|
||||
return None
|
||||
|
||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
DOLLAR_SKILLS_AGENTS: frozenset[str] = frozenset({"codex", "zcode"})
|
||||
|
||||
# Agents that always render /speckit-<name>, regardless of ai_skills.
|
||||
ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset({"devin", "droid", "grok", "trae", "zed"})
|
||||
ALWAYS_SLASH_AGENTS: frozenset[str] = frozenset({"devin", "grok", "trae", "zed"})
|
||||
|
||||
# Agents that render /speckit-<name> only when ai_skills is enabled.
|
||||
CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset(
|
||||
@@ -29,9 +29,6 @@ CONDITIONAL_SLASH_AGENTS: frozenset[str] = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
# Agents that render /skill:<name> (skill-colon invocation) when in skills mode.
|
||||
SKILL_COLON_AGENTS: frozenset[str] = frozenset({"kimi"})
|
||||
|
||||
|
||||
def is_dollar_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool:
|
||||
"""Return ``True`` if *selected_ai* uses ``$speckit-<name>`` invocations.
|
||||
@@ -44,21 +41,6 @@ def is_dollar_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) ->
|
||||
return selected_ai in DOLLAR_SKILLS_AGENTS and ai_skills_enabled
|
||||
|
||||
|
||||
def get_invocation_prefix(selected_ai: str | None, ai_skills_enabled: bool) -> str:
|
||||
"""Return the native invocation prefix for *selected_ai* in skills mode.
|
||||
|
||||
Returns ``"$"`` for dollar-skills agents (Codex, ZCode),
|
||||
``"/skill:"`` for skill-colon agents (Kimi), and ``"/"`` for all others.
|
||||
"""
|
||||
if not isinstance(selected_ai, str):
|
||||
return "/"
|
||||
if selected_ai in DOLLAR_SKILLS_AGENTS and ai_skills_enabled:
|
||||
return "$"
|
||||
if selected_ai in SKILL_COLON_AGENTS and ai_skills_enabled:
|
||||
return "/skill:"
|
||||
return "/"
|
||||
|
||||
|
||||
def is_slash_skills_agent(selected_ai: str | None, ai_skills_enabled: bool) -> bool:
|
||||
"""Return ``True`` if *selected_ai* uses ``/speckit-<name>`` invocations.
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import yaml
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from typing import Any
|
||||
from ._console import console
|
||||
from ._download_security import normalize_zip_member_name
|
||||
|
||||
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
||||
CLAUDE_NPM_LOCAL_PATH = Path.home() / ".claude" / "local" / "node_modules" / ".bin" / "claude"
|
||||
@@ -28,22 +27,19 @@ def relative_extension_path_violation(value: Any) -> str | None:
|
||||
``None`` when it is an acceptable relative path within the extension
|
||||
directory.
|
||||
|
||||
Policy: the value must be a non-empty, portable file path with no
|
||||
leading/trailing whitespace, absolute/anchored form, ``..`` traversal,
|
||||
platform-reserved component, or directory-only suffix. The value is
|
||||
Policy: the value must be a non-empty string with no leading/trailing
|
||||
whitespace, no absolute/anchored form, and no ``..`` traversal. The value is
|
||||
evaluated under both POSIX and Windows path semantics because a native
|
||||
``Path`` is OS-dependent (a ``PurePosixPath`` on POSIX does not interpret
|
||||
Windows drive/UNC forms, and ``C:foo`` is anchored but not
|
||||
``is_absolute()`` yet resolves against the CWD on its drive). Rejecting any
|
||||
non-empty anchor covers POSIX-absolute (``/abs``), Windows drive-relative
|
||||
(``C:foo``), Windows absolute (``C:\\foo``), and UNC/rooted forms.
|
||||
Windows drive/UNC forms, and ``C:foo`` is anchored but not ``is_absolute()``
|
||||
yet resolves against the CWD on its drive). Rejecting any non-empty anchor
|
||||
covers POSIX-absolute (``/abs``), Windows drive-relative (``C:foo``), Windows
|
||||
absolute (``C:\\foo``), and UNC/rooted forms.
|
||||
"""
|
||||
if not isinstance(value, str) or not value:
|
||||
return "must be a non-empty string"
|
||||
if value.strip() != value:
|
||||
return "must not have leading or trailing whitespace"
|
||||
if "\\" in value:
|
||||
return "must use forward slashes as path separators"
|
||||
posix_path = PurePosixPath(value)
|
||||
win_path = PureWindowsPath(value)
|
||||
if (
|
||||
@@ -56,15 +52,6 @@ def relative_extension_path_violation(value: Any) -> str | None:
|
||||
"must be a relative path within the extension directory "
|
||||
"(no absolute paths, drive letters, or '..' segments)"
|
||||
)
|
||||
if value.endswith(("/", "\\")):
|
||||
return "must name a file or command, not a directory"
|
||||
try:
|
||||
normalize_zip_member_name(value)
|
||||
except ValueError:
|
||||
return (
|
||||
"must use portable path components "
|
||||
"(no reserved names or platform-invalid characters)"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -82,14 +69,21 @@ 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.
|
||||
|
||||
Commands are always executed with ``shell=False`` and must be passed as an
|
||||
argv ``list[str]``. There is deliberately no ``shell`` parameter: the
|
||||
argv-list contract makes shell interpolation impossible by construction, so
|
||||
the shell-injection surface cannot be re-enabled at a call site.
|
||||
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"
|
||||
)
|
||||
|
||||
try:
|
||||
if capture:
|
||||
result = subprocess.run(cmd, check=check_return, capture_output=True, text=True)
|
||||
|
||||
@@ -4,8 +4,8 @@ Pure helpers for comparing PEP 440 versions and fetching the latest GitHub
|
||||
release tag. The ``self_app`` Typer sub-command group is co-located here so
|
||||
all version-related logic lives in one place.
|
||||
|
||||
Dependencies: stdlib + packaging + ._console + ._download_security only
|
||||
(keeping this layer thin and circular-import-safe).
|
||||
Dependencies: stdlib + packaging + ._console only (no other internal imports
|
||||
at module level, keeping this layer thin and circular-import-safe).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -27,9 +27,7 @@ from pathlib import Path
|
||||
|
||||
import typer
|
||||
from packaging.version import InvalidVersion, Version
|
||||
from rich.markup import escape as _escape_markup
|
||||
|
||||
from ._download_security import MAX_JSON_METADATA_BYTES, read_response_limited
|
||||
from ._console import console
|
||||
|
||||
GITHUB_API_LATEST = "https://api.github.com/repos/github/spec-kit/releases/latest"
|
||||
@@ -121,13 +119,7 @@ def _fetch_latest_release_tag() -> tuple[str | None, str | None]:
|
||||
timeout=5,
|
||||
extra_headers={"Accept": "application/vnd.github+json"},
|
||||
) as resp:
|
||||
payload = json.loads(
|
||||
read_response_limited(
|
||||
resp,
|
||||
max_bytes=MAX_JSON_METADATA_BYTES,
|
||||
label="GitHub latest release",
|
||||
).decode("utf-8")
|
||||
)
|
||||
payload = json.loads(resp.read().decode("utf-8"))
|
||||
tag = payload.get("tag_name")
|
||||
if not isinstance(tag, str) or not tag:
|
||||
raise ValueError("GitHub API response missing valid tag_name")
|
||||
@@ -1231,10 +1223,7 @@ def self_upgrade(
|
||||
tag: str | None = typer.Option(
|
||||
None,
|
||||
"--tag",
|
||||
# Typer renders help through Rich, so escape the literal bracket (\[)
|
||||
# or `[suffix]` is parsed as a style tag and dropped -- `--help` then
|
||||
# advertises only `(vX.Y.Z)`, contradicting docs/upgrade.md and README.
|
||||
help="Pin the target version (vX.Y.Z\\[suffix]). Without --tag, the "
|
||||
help="Pin the target version (vX.Y.Z[suffix]). Without --tag, the "
|
||||
"latest stable release is resolved via GitHub Releases.",
|
||||
),
|
||||
) -> None:
|
||||
@@ -1274,14 +1263,7 @@ def self_upgrade(
|
||||
try:
|
||||
tag = _validate_tag(tag)
|
||||
except typer.BadParameter as exc:
|
||||
# Escape at the print site rather than baking `\[` into
|
||||
# _INVALID_TAG_MESSAGE: the message is also raised through
|
||||
# typer.BadParameter, which Click renders without Rich, so the
|
||||
# constant must stay plain text. Unescaped, Rich parses the literal
|
||||
# `[suffix]` as a style tag and drops it, leaving the user with
|
||||
# "expected vMAJOR.MINOR.PATCH" -- implying a bare vX.Y.Z is the only
|
||||
# accepted form when -rc1 / .dev0 / +build.42 are all valid.
|
||||
console.print(_escape_markup(str(exc)), soft_wrap=True)
|
||||
console.print(str(exc), soft_wrap=True)
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
plan, failure_reason = _build_upgrade_plan(target_tag_override=tag)
|
||||
|
||||
@@ -10,12 +10,11 @@ import os
|
||||
import re
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
from ._init_options import is_ai_skills_enabled, load_init_options
|
||||
from ._invocation_style import get_invocation_prefix
|
||||
from ._toml_string import escape_toml_basic as _escape_toml_basic
|
||||
from ._toml_string import has_illegal_toml_control as _has_illegal_toml_control
|
||||
from ._utils import relative_extension_path_violation
|
||||
@@ -271,7 +270,7 @@ class CommandRegistrar:
|
||||
return text
|
||||
|
||||
def render_markdown_command(
|
||||
self, frontmatter: dict, body: str, source_id: str, context_note: Optional[str] = None
|
||||
self, frontmatter: dict, body: str, source_id: str, context_note: str = None
|
||||
) -> str:
|
||||
"""Render command in Markdown format.
|
||||
|
||||
@@ -302,20 +301,8 @@ class CommandRegistrar:
|
||||
toml_lines = []
|
||||
|
||||
if "description" in frontmatter:
|
||||
# Frontmatter comes from ``yaml.safe_load``, so ``description`` can
|
||||
# be any YAML type: ``description:`` with no value yields None,
|
||||
# ``description: 2`` an int, an unquoted ``true`` a bool.
|
||||
# ``_render_basic_toml_string`` iterates the value and calls ord()
|
||||
# on each character, so a non-string raises a raw TypeError -- and a
|
||||
# list of single-character items is silently concatenated into a
|
||||
# wrong value (``["a", "b"]`` -> ``"ab"``). Coerce first, matching
|
||||
# ``render_yaml_command`` below and ``TomlIntegration
|
||||
# ._extract_description``, which both normalise it already.
|
||||
description = frontmatter["description"]
|
||||
if not isinstance(description, str):
|
||||
description = str(description) if description is not None else ""
|
||||
toml_lines.append(
|
||||
f"description = {self._render_basic_toml_string(description)}"
|
||||
f"description = {self._render_basic_toml_string(frontmatter['description'])}"
|
||||
)
|
||||
toml_lines.append("")
|
||||
|
||||
@@ -610,8 +597,8 @@ class CommandRegistrar:
|
||||
source_id: str,
|
||||
source_dir: Path,
|
||||
project_root: Path,
|
||||
context_note: Optional[str] = None,
|
||||
_resolved_dir: Optional[Path] = None,
|
||||
context_note: str = None,
|
||||
_resolved_dir: Path = None,
|
||||
link_outputs: bool = False,
|
||||
extension_id: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
@@ -672,38 +659,22 @@ class CommandRegistrar:
|
||||
# correct when a stale ``.bob/skills`` directory coexists with
|
||||
# ``.bob/commands``.
|
||||
_sep = agent_config.get("invoke_separator", ".")
|
||||
registrar_writes_skills = agent_config.get("extension") == "/SKILL.md"
|
||||
try:
|
||||
from specify_cli.integrations import get_integration # noqa: PLC0415
|
||||
|
||||
_integ = get_integration(agent_name)
|
||||
if _integ is not None:
|
||||
registrar_writes_skills = (
|
||||
agent_config.get("extension") == "/SKILL.md"
|
||||
)
|
||||
_sep = _integ.invoke_separator_for_mode(registrar_writes_skills)
|
||||
except Exception:
|
||||
pass
|
||||
_prefix = get_invocation_prefix(agent_name, registrar_writes_skills)
|
||||
|
||||
for cmd_info in commands:
|
||||
cmd_name = cmd_info["name"]
|
||||
aliases = cmd_info.get("aliases", [])
|
||||
cmd_file = cmd_info["file"]
|
||||
name_reason = relative_extension_path_violation(cmd_name)
|
||||
if name_reason:
|
||||
raise ValueError(
|
||||
f"Invalid command name {cmd_name!r}: {name_reason}"
|
||||
)
|
||||
if aliases is None:
|
||||
aliases = []
|
||||
if not isinstance(aliases, list):
|
||||
raise ValueError(
|
||||
f"Aliases for command {cmd_name!r} must be a list"
|
||||
)
|
||||
for alias in aliases:
|
||||
alias_reason = relative_extension_path_violation(alias)
|
||||
if alias_reason:
|
||||
raise ValueError(
|
||||
f"Invalid command alias {alias!r}: {alias_reason}"
|
||||
)
|
||||
|
||||
# Guard against path traversal using the single shared policy in
|
||||
# relative_extension_path_violation(), so the runtime guard stays
|
||||
@@ -784,7 +755,7 @@ class CommandRegistrar:
|
||||
# (base.py itself imports CommandRegistrar lazily).
|
||||
from specify_cli.integrations.base import IntegrationBase # noqa: PLC0415
|
||||
|
||||
body = IntegrationBase.resolve_command_refs(body, _sep, _prefix)
|
||||
body = IntegrationBase.resolve_command_refs(body, _sep)
|
||||
|
||||
output_name = self._compute_output_name(agent_name, cmd_name, agent_config)
|
||||
|
||||
@@ -986,16 +957,10 @@ class CommandRegistrar:
|
||||
project_root: Path to project root
|
||||
cmd_name: Command name (e.g. 'speckit.my-ext.example')
|
||||
"""
|
||||
name_reason = relative_extension_path_violation(cmd_name)
|
||||
if name_reason:
|
||||
raise ValueError(
|
||||
f"Invalid Copilot prompt name {cmd_name!r}: {name_reason}"
|
||||
)
|
||||
prompts_dir = project_root / ".github" / "prompts"
|
||||
prompts_dir.mkdir(parents=True, exist_ok=True)
|
||||
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
|
||||
CommandRegistrar._ensure_inside(prompt_file, prompts_dir)
|
||||
prompt_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
|
||||
|
||||
@staticmethod
|
||||
@@ -1051,11 +1016,10 @@ class CommandRegistrar:
|
||||
source_id: str,
|
||||
source_dir: Path,
|
||||
project_root: Path,
|
||||
context_note: Optional[str] = None,
|
||||
context_note: str = None,
|
||||
link_outputs: bool = False,
|
||||
create_missing_active_skills_dir: bool = False,
|
||||
extension_id: Optional[str] = None,
|
||||
only_agent: Optional[str] = None,
|
||||
) -> Dict[str, List[str]]:
|
||||
"""Register commands for all detected agents in the project.
|
||||
|
||||
@@ -1073,8 +1037,6 @@ class CommandRegistrar:
|
||||
skills directory) and is skipped when safe resolution or
|
||||
creation fails.
|
||||
extension_id: Extension id when rendering extension-owned commands.
|
||||
only_agent: If set, restrict registration to this single agent
|
||||
while keeping all detection and recovery safeguards (#2948).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping agent names to list of registered commands
|
||||
@@ -1098,8 +1060,6 @@ class CommandRegistrar:
|
||||
)
|
||||
active_created_skills_dir: Optional[Path] = None
|
||||
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
||||
if only_agent is not None and agent_name != only_agent:
|
||||
continue
|
||||
active_skills_output = (
|
||||
agent_name == active_skills_agent
|
||||
and agent_config.get("extension") == "/SKILL.md"
|
||||
@@ -1205,8 +1165,6 @@ class CommandRegistrar:
|
||||
context_note: Optional[str] = None,
|
||||
link_outputs: bool = False,
|
||||
extension_id: Optional[str] = None,
|
||||
only_agent: Optional[str] = None,
|
||||
extra_agents: Optional[Iterable[str]] = None,
|
||||
) -> Dict[str, List[str]]:
|
||||
"""Register commands for all non-skill agents in the project.
|
||||
|
||||
@@ -1223,29 +1181,13 @@ class CommandRegistrar:
|
||||
link_outputs: If True, create dev-mode symlinks for rendered
|
||||
command files when supported by the OS.
|
||||
extension_id: Extension id when rendering extension-owned commands.
|
||||
only_agent: If set, restrict registration to this single agent
|
||||
(#2948). An agent name that matches no configured agent
|
||||
(e.g. an empty string) yields no registrations at all.
|
||||
extra_agents: Additional agent names to register for besides
|
||||
``only_agent``. Used by post-removal reconciliation to also
|
||||
restore surviving content into historical agent directories
|
||||
a just-removed preset actually wrote to, not only the
|
||||
currently active agent (#2948). Ignored when ``only_agent``
|
||||
is ``None`` (already unrestricted).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping agent names to list of registered commands
|
||||
"""
|
||||
results = {}
|
||||
self._ensure_configs()
|
||||
extra_agents_set = frozenset(extra_agents) if extra_agents else frozenset()
|
||||
for agent_name, agent_config in self.AGENT_CONFIGS.items():
|
||||
if (
|
||||
only_agent is not None
|
||||
and agent_name != only_agent
|
||||
and agent_name not in extra_agents_set
|
||||
):
|
||||
continue
|
||||
if agent_config.get("extension") == "/SKILL.md":
|
||||
continue
|
||||
detect_dir_str = agent_config.get("detect_dir")
|
||||
|
||||
@@ -5,11 +5,9 @@ from __future__ import annotations
|
||||
import base64
|
||||
import json as _json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .._download_security import MAX_JSON_METADATA_BYTES, read_response_limited
|
||||
from .base import AuthProvider
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -19,20 +17,6 @@ if TYPE_CHECKING:
|
||||
_ADO_RESOURCE_ID = "499b84ac-1321-427f-aa17-267ca6975798"
|
||||
|
||||
|
||||
class _TokenResponseTooLarge(Exception):
|
||||
"""Raised when an Azure AD token response exceeds the bounded read limit."""
|
||||
|
||||
|
||||
def _extract_token(payload: object, key: str) -> str | None:
|
||||
"""Return a normalized token from a JSON object, or None for other shapes."""
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
token = payload.get(key)
|
||||
if not isinstance(token, str):
|
||||
return None
|
||||
return token.strip() or None
|
||||
|
||||
|
||||
class AzureDevOpsAuth(AuthProvider):
|
||||
"""Azure DevOps authentication provider.
|
||||
|
||||
@@ -72,27 +56,9 @@ class AzureDevOpsAuth(AuthProvider):
|
||||
def _acquire_via_az_cli() -> str | None:
|
||||
"""Run ``az account get-access-token`` and return the access token."""
|
||||
try:
|
||||
# Windows: ``subprocess.run`` calls ``CreateProcess``, which does
|
||||
# not consult ``PATHEXT``, so a bare ``"az"`` (installed as
|
||||
# ``az.cmd``) fails with ``WinError 2`` even after ``az login``.
|
||||
# Resolve via ``shutil.which`` (which honors ``PATHEXT``) so the
|
||||
# ``.cmd`` shim works. On POSIX this is a harmless lookup that
|
||||
# returns the same executable.
|
||||
#
|
||||
# Require an ABSOLUTE result: on Windows ``shutil.which`` prepends
|
||||
# the current directory to the search path (unless
|
||||
# ``NoDefaultCurrentDirectoryInExePath`` is set), so a stray
|
||||
# ``.\az.cmd`` in the working directory would otherwise be resolved
|
||||
# ahead of the real Azure CLI and run for a credential operation. A
|
||||
# legitimate install always resolves to an absolute path, so this
|
||||
# costs nothing; falling back to the bare ``"az"`` preserves the
|
||||
# prior behavior (and the existing OSError path) when ``az`` is
|
||||
# absent.
|
||||
resolved = shutil.which("az")
|
||||
az = resolved if resolved and os.path.isabs(resolved) else "az"
|
||||
result = subprocess.run( # noqa: S603, S607
|
||||
[
|
||||
az,
|
||||
"az",
|
||||
"account",
|
||||
"get-access-token",
|
||||
"--resource",
|
||||
@@ -108,7 +74,8 @@ class AzureDevOpsAuth(AuthProvider):
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
payload = _json.loads(result.stdout)
|
||||
return _extract_token(payload, "accessToken")
|
||||
token = payload.get("accessToken", "").strip()
|
||||
return token or None
|
||||
except (
|
||||
OSError,
|
||||
subprocess.TimeoutExpired,
|
||||
@@ -152,37 +119,9 @@ class AzureDevOpsAuth(AuthProvider):
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
try:
|
||||
from specify_cli.authentication.http import _StripAuthOnRedirect
|
||||
|
||||
def reject_token_redirect(_old_url: str, new_url: str) -> None:
|
||||
# A 307/308 redirect preserves this POST body, including the
|
||||
# client_secret. Refuse every redirect so credentials cannot
|
||||
# leave the fixed Microsoft token endpoint.
|
||||
raise urllib.error.URLError(
|
||||
f"Azure AD token request must not be redirected to {new_url}"
|
||||
)
|
||||
|
||||
opener = urllib.request.build_opener(
|
||||
_StripAuthOnRedirect((), reject_token_redirect)
|
||||
)
|
||||
with opener.open(req, timeout=30) as resp: # noqa: S310
|
||||
payload = _json.loads(
|
||||
read_response_limited(
|
||||
resp,
|
||||
max_bytes=MAX_JSON_METADATA_BYTES,
|
||||
error_type=_TokenResponseTooLarge,
|
||||
label="Azure DevOps token response",
|
||||
).decode("utf-8")
|
||||
)
|
||||
return _extract_token(payload, "access_token")
|
||||
except (
|
||||
urllib.error.URLError,
|
||||
OSError,
|
||||
_json.JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
_TokenResponseTooLarge,
|
||||
):
|
||||
# Network failure, malformed JSON, or an oversized response — fall
|
||||
# through to the next strategy. Unrelated programming errors (other
|
||||
# ValueErrors, KeyErrors) intentionally propagate so they surface.
|
||||
with urllib.request.urlopen(req, timeout=30) as resp: # noqa: S310
|
||||
payload = _json.loads(resp.read().decode("utf-8"))
|
||||
token = payload.get("access_token", "").strip()
|
||||
return token or None
|
||||
except (urllib.error.URLError, OSError, _json.JSONDecodeError, KeyError):
|
||||
return None
|
||||
|
||||
@@ -13,7 +13,6 @@ import stat
|
||||
from dataclasses import dataclass
|
||||
from fnmatch import fnmatch
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
@@ -54,19 +53,6 @@ def _is_valid_host_pattern(pattern: str) -> bool:
|
||||
return pattern.startswith("*.") and "*" not in pattern[2:]
|
||||
|
||||
|
||||
def _norm(value: Any) -> Any:
|
||||
"""Strip surrounding whitespace from a whitespace-insignificant string
|
||||
config reference (env-var names, tenant/client ids) before it is stored.
|
||||
|
||||
These fields are validated on their ``.strip()``ed form, so an accidentally
|
||||
padded value passes validation but then silently breaks the verbatim
|
||||
``os.environ.get(...)`` / URL lookups downstream. Normalizing at store time
|
||||
mirrors how ``hosts`` is already handled (``h.strip().lower()``). Non-string
|
||||
values (e.g. ``None``) pass through unchanged.
|
||||
"""
|
||||
return value.strip() if isinstance(value, str) else value
|
||||
|
||||
|
||||
def load_auth_config(
|
||||
path: Path | None = None,
|
||||
) -> list[AuthConfigEntry]:
|
||||
@@ -196,10 +182,10 @@ def load_auth_config(
|
||||
provider=provider,
|
||||
auth=auth,
|
||||
token=token,
|
||||
token_env=_norm(token_env),
|
||||
tenant_id=_norm(entry_raw.get("tenant_id")),
|
||||
client_id=_norm(entry_raw.get("client_id")),
|
||||
client_secret_env=_norm(entry_raw.get("client_secret_env")),
|
||||
token_env=token_env,
|
||||
tenant_id=entry_raw.get("tenant_id"),
|
||||
client_id=entry_raw.get("client_id"),
|
||||
client_secret_env=entry_raw.get("client_secret_env"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ from fnmatch import fnmatch
|
||||
from typing import Callable
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from .._download_security import is_safe_download_redirect
|
||||
from . import get_provider
|
||||
from .config import AuthConfigEntry, _default_config_path, find_entries_for_url, load_auth_config
|
||||
|
||||
@@ -61,23 +60,8 @@ def _hostname_in_hosts(hostname: str, hosts: tuple[str, ...]) -> bool:
|
||||
RedirectValidator = Callable[[str, str], None]
|
||||
|
||||
|
||||
def _validate_strict_redirect(old_url: str, new_url: str) -> None:
|
||||
if not is_safe_download_redirect(old_url, new_url):
|
||||
raise urllib.error.URLError(
|
||||
f"unsafe redirect to {new_url}: target must use HTTPS with a hostname, "
|
||||
"must not enter a local target from a remote host, and may use HTTP only "
|
||||
"within loopback (for example localhost, 127.0.0.1, ::1)"
|
||||
)
|
||||
|
||||
|
||||
class _StripAuthOnRedirect(urllib.request.HTTPRedirectHandler):
|
||||
"""Redirect handler that guards every redirect it is installed for.
|
||||
|
||||
1. Run any caller-provided redirect validator.
|
||||
2. Reject redirects that are not HTTPS with a hostname. HTTP loopback is
|
||||
allowed only when the previous hop is also loopback.
|
||||
3. Drop ``Authorization`` when a redirect leaves trusted hosts or downgrades.
|
||||
"""
|
||||
"""Drop ``Authorization`` when a redirect leaves trusted hosts or downgrades."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -91,8 +75,6 @@ class _StripAuthOnRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
||||
try:
|
||||
new_parsed = urlparse(newurl)
|
||||
# Force urllib's syntax and range validation before following.
|
||||
new_parsed.port
|
||||
except ValueError as exc:
|
||||
# Malformed redirect target (e.g. unterminated IPv6 bracket).
|
||||
# Surface as URLError so callers' download error handling applies.
|
||||
@@ -100,7 +82,6 @@ class _StripAuthOnRedirect(urllib.request.HTTPRedirectHandler):
|
||||
|
||||
if self._redirect_validator is not None:
|
||||
self._redirect_validator(req.full_url, newurl)
|
||||
_validate_strict_redirect(req.full_url, newurl)
|
||||
|
||||
original_auth = (
|
||||
req.get_header("Authorization")
|
||||
@@ -174,12 +155,6 @@ def open_url(
|
||||
*extra_headers* (e.g. ``Accept``) are merged into every attempt.
|
||||
*redirect_validator*, when provided, is called with ``(old_url, new_url)``
|
||||
before following each redirect and may raise to reject the redirect.
|
||||
|
||||
Every attempt uses an isolated opener so a process-wide opener installed
|
||||
with ``urllib.request.install_opener`` cannot replace the redirect guard.
|
||||
Redirect scheme safety: every attempt goes through
|
||||
``_StripAuthOnRedirect``, which rejects redirects to non-HTTPS URLs except
|
||||
HTTP between loopback URLs, and rejects remote-to-local redirects.
|
||||
"""
|
||||
entries = find_entries_for_url(url, _load_config())
|
||||
|
||||
@@ -213,7 +188,7 @@ def open_url(
|
||||
|
||||
# No entry worked (or none matched) — unauthenticated fallback
|
||||
req = _make_req({})
|
||||
# No auth is attached on this path, so the handler's host list is empty:
|
||||
# here it runs redirect validation only, not auth stripping.
|
||||
opener = urllib.request.build_opener(_StripAuthOnRedirect((), redirect_validator))
|
||||
return opener.open(req, timeout=timeout)
|
||||
if redirect_validator is not None:
|
||||
opener = urllib.request.build_opener(_StripAuthOnRedirect((), redirect_validator))
|
||||
return opener.open(req, timeout=timeout)
|
||||
return urllib.request.urlopen(req, timeout=timeout) # noqa: S310
|
||||
|
||||
@@ -14,13 +14,14 @@ from .. import BundlerError
|
||||
from ..lib.yamlio import dump_yaml, ensure_within, load_yaml
|
||||
from ..models.catalog import (
|
||||
CONFIG_FILENAME,
|
||||
CONFIG_SCHEMA_VERSION,
|
||||
BUILTIN_DEFAULT_STACK,
|
||||
CatalogSource,
|
||||
InstallPolicy,
|
||||
Scope,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA_VERSION = "1.0"
|
||||
|
||||
_BUILTIN_IDS = {raw["id"] for raw in BUILTIN_DEFAULT_STACK}
|
||||
|
||||
# Windows absolute paths like ``C:\catalog.json`` parse with a single-letter
|
||||
@@ -39,12 +40,9 @@ def _read(project_root: Path) -> list[dict]:
|
||||
path = ensure_within(project_root, _config_path(project_root))
|
||||
if not path.exists():
|
||||
return []
|
||||
# ``load_yaml`` returns ``{}`` only for an empty document and the raw parse
|
||||
# otherwise, so a non-mapping top level — a falsy ``[]``/``false``/``0``/``''``
|
||||
# or an explicit null (``load_yaml`` -> ``None``) — is caught by the isinstance
|
||||
# guard below and raised like a truthy one, staying consistent with the other
|
||||
# reader of this file (models/catalog._merge_config).
|
||||
data = load_yaml(path)
|
||||
if data is None:
|
||||
return []
|
||||
if not isinstance(data, dict):
|
||||
raise BundlerError(
|
||||
f"Malformed catalog config at {path}: expected a mapping at the top "
|
||||
@@ -145,15 +143,6 @@ def add_source(
|
||||
raise BundlerError("A catalog url is required.")
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
# Read .hostname inside the try: a bracketed-but-invalid IPv6 authority
|
||||
# (e.g. "https://[not-an-ip]/c.json") parses cleanly under urlparse() on
|
||||
# Python < 3.14 but raises ValueError lazily on the first .hostname access
|
||||
# (the raise moved eager into urlparse() only in 3.14). Reading it here
|
||||
# keeps that ValueError inside the guard instead of leaking a raw
|
||||
# traceback past the CLI's `except BundlerError`. Reuse the value below.
|
||||
hostname = parsed.hostname
|
||||
# Accessing ``port`` performs urllib's syntax/range validation.
|
||||
_ = parsed.port
|
||||
except ValueError as exc:
|
||||
raise BundlerError(f"Invalid catalog url: '{url}'.") from exc
|
||||
if not (parsed.scheme or parsed.path):
|
||||
@@ -172,13 +161,13 @@ def add_source(
|
||||
# netloc — netloc is truthy for host-less URLs like "https://:8080"
|
||||
# or "https://user@". Validating here keeps junk out of
|
||||
# bundle-catalogs.yml instead of failing later at fetch time.
|
||||
is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
|
||||
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
|
||||
if parsed.scheme.lower() != "https" and not is_localhost:
|
||||
raise BundlerError(
|
||||
f"Catalog url must use HTTPS (got {parsed.scheme}://). "
|
||||
"HTTP is only allowed for localhost."
|
||||
)
|
||||
if not hostname:
|
||||
if not parsed.hostname:
|
||||
raise BundlerError(f"Catalog url must be a valid URL with a host: {url}")
|
||||
|
||||
url = _canonicalize_url(url)
|
||||
|
||||
@@ -39,40 +39,17 @@ def ensure_within(root: Path, candidate: Path) -> Path:
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> Any:
|
||||
"""Parse a YAML file, returning ``{}`` only for an *empty* document.
|
||||
|
||||
A non-empty document is returned exactly as parsed — including a
|
||||
non-mapping such as ``[]``, ``false``, ``0``, ``''``, or an explicit null
|
||||
(``null``/``~``) — so callers can validate the top-level shape (e.g. reject
|
||||
a non-mapping config) instead of having it silently coerced to an empty
|
||||
mapping.
|
||||
|
||||
``yaml.safe_load`` returns ``None`` for *both* an empty document and an
|
||||
explicit null scalar, so ``yaml.compose`` (which yields no node only for a
|
||||
truly empty document) is used to tell them apart: an empty document becomes
|
||||
``{}`` while an explicit ``null``/``~`` is returned as ``None`` for the
|
||||
caller to reject.
|
||||
"""
|
||||
"""Parse a YAML file, returning ``{}`` for an empty document."""
|
||||
path = Path(path)
|
||||
if not path.exists():
|
||||
raise BundlerError(f"File not found: {path}")
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeError) as exc:
|
||||
# A non-UTF-8 file raises UnicodeDecodeError, which is a ValueError --
|
||||
# NOT an OSError -- so it escaped this module's "IO failures degrade
|
||||
# into actionable BundlerError" contract as a raw traceback. Realistic
|
||||
# on Windows, where PowerShell 5.1's `Out-File`/`>` default to UTF-16.
|
||||
# Matches the sibling catalog readers (catalogs.py, workflows/catalog.py).
|
||||
raise BundlerError(f"Could not read {path}: {exc}") from exc
|
||||
try:
|
||||
has_node = yaml.compose(text) is not None
|
||||
data = yaml.safe_load(text)
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return yaml.safe_load(handle) or {}
|
||||
except yaml.YAMLError as exc:
|
||||
raise BundlerError(f"Invalid YAML in {path}: {exc}") from exc
|
||||
if data is None and not has_node:
|
||||
return {}
|
||||
return data
|
||||
except OSError as exc:
|
||||
raise BundlerError(f"Could not read {path}: {exc}") from exc
|
||||
|
||||
|
||||
def dump_yaml(path: Path, data: Any, *, within: Path | None = None) -> Path:
|
||||
@@ -83,13 +60,7 @@ def dump_yaml(path: Path, data: Any, *, within: Path | None = None) -> Path:
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8") as handle:
|
||||
yaml.safe_dump(
|
||||
data,
|
||||
handle,
|
||||
sort_keys=False,
|
||||
default_flow_style=False,
|
||||
allow_unicode=True,
|
||||
)
|
||||
yaml.safe_dump(data, handle, sort_keys=False, default_flow_style=False)
|
||||
except OSError as exc:
|
||||
raise BundlerError(f"Could not write {path}: {exc}") from exc
|
||||
return path
|
||||
@@ -103,15 +74,9 @@ def load_json(path: Path) -> Any:
|
||||
try:
|
||||
with path.open("r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
# JSONDecodeError stays FIRST: it and UnicodeDecodeError are sibling
|
||||
# ValueError subclasses (neither subsumes the other), so malformed-but-
|
||||
# decodable JSON keeps its more specific "Invalid JSON" message while a
|
||||
# decode failure falls through to the read-error clause below.
|
||||
except json.JSONDecodeError as exc:
|
||||
raise BundlerError(f"Invalid JSON in {path}: {exc}") from exc
|
||||
except (OSError, UnicodeError) as exc:
|
||||
# See load_yaml: a non-UTF-8 file raises UnicodeDecodeError, which is
|
||||
# not an OSError, and previously escaped as a raw traceback.
|
||||
except OSError as exc:
|
||||
raise BundlerError(f"Could not read {path}: {exc}") from exc
|
||||
|
||||
|
||||
|
||||
@@ -15,11 +15,6 @@ from .. import BundlerError
|
||||
from ..lib.yamlio import ensure_within, load_yaml
|
||||
|
||||
CONFIG_FILENAME = "bundle-catalogs.yml"
|
||||
# Supported bundle-catalogs.yml schema (major version). Both readers of the
|
||||
# file — this module's _merge_config and commands_impl/catalog_config._read —
|
||||
# reject an unsupported major version so a file written by a newer/incompatible
|
||||
# Spec Kit fails fast instead of being parsed under the wrong assumptions.
|
||||
CONFIG_SCHEMA_VERSION = "1.0"
|
||||
|
||||
|
||||
class InstallPolicy(str, Enum):
|
||||
@@ -144,7 +139,6 @@ class CatalogEntry:
|
||||
license: str
|
||||
download_url: str
|
||||
requires_speckit_version: str
|
||||
sha256: str | None = None
|
||||
provides: dict[str, int] = field(default_factory=dict)
|
||||
repository: str | None = None
|
||||
tags: tuple[str, ...] = ()
|
||||
@@ -158,21 +152,14 @@ class CatalogEntry:
|
||||
if not isinstance(data, dict):
|
||||
raise BundlerError("Each catalog entry must be a mapping.")
|
||||
entry_id = str(data.get("id", "")).strip()
|
||||
# `or {}` would coerce a FALSY non-mapping (0, '', False, []) to {} before
|
||||
# the isinstance guard, silently accepting a corrupt catalog entry; only
|
||||
# an absent/None value means "not present".
|
||||
requires = data.get("requires")
|
||||
if requires is None:
|
||||
requires = {}
|
||||
elif not isinstance(requires, dict):
|
||||
requires = data.get("requires") or {}
|
||||
if not isinstance(requires, dict):
|
||||
raise BundlerError(
|
||||
f"Catalog entry '{entry_id or '<unknown>'}': 'requires' must be a "
|
||||
"mapping when present."
|
||||
)
|
||||
provides_raw = data.get("provides")
|
||||
if provides_raw is None:
|
||||
provides_raw = {}
|
||||
elif not isinstance(provides_raw, dict):
|
||||
provides_raw = data.get("provides") or {}
|
||||
if not isinstance(provides_raw, dict):
|
||||
raise BundlerError(
|
||||
f"Catalog entry '{entry_id or '<unknown>'}': 'provides' must be a "
|
||||
"mapping when present."
|
||||
@@ -187,11 +174,6 @@ class CatalogEntry:
|
||||
license=str(data.get("license", "")).strip(),
|
||||
download_url=str(data.get("download_url", "")).strip(),
|
||||
requires_speckit_version=str(requires.get("speckit_version", "")).strip(),
|
||||
sha256=(
|
||||
None
|
||||
if data.get("sha256") is None
|
||||
else str(data["sha256"]).strip()
|
||||
),
|
||||
provides=dict(provides_raw),
|
||||
repository=(str(data["repository"]) if data.get("repository") else None),
|
||||
tags=_parse_tags(data.get("tags"), entry_id),
|
||||
@@ -204,7 +186,6 @@ class CatalogEntry:
|
||||
description=self.description, author=self.author, license=self.license,
|
||||
download_url=self.download_url,
|
||||
requires_speckit_version=self.requires_speckit_version,
|
||||
sha256=self.sha256,
|
||||
provides=self.provides, repository=self.repository, tags=self.tags,
|
||||
verified=self.verified, source_id=source.id,
|
||||
source_policy=source.install_policy,
|
||||
@@ -268,51 +249,10 @@ def load_source_stack(project_root: Path, user_config_dir: Path | None = None) -
|
||||
def _merge_config(by_id: dict[str, CatalogSource], config_path: Path, scope: Scope) -> None:
|
||||
if not config_path.exists():
|
||||
return
|
||||
# ``load_yaml`` returns ``{}`` only for an empty document and the raw parse
|
||||
# otherwise, so a non-mapping top level (a YAML list or scalar, including
|
||||
# the falsy ``[]``/``false``/``0``/``''``) is caught here and raised —
|
||||
# matching the sibling reader commands_impl/catalog_config._read. #3623
|
||||
# aligned the inner non-list ``catalogs`` value between the two readers.
|
||||
data = load_yaml(config_path)
|
||||
if not isinstance(data, dict):
|
||||
raise BundlerError(
|
||||
f"Malformed catalog config at {config_path}: expected a mapping at "
|
||||
f"the top level, got {type(data).__name__}."
|
||||
)
|
||||
# Reject an unsupported major schema version, matching the sibling reader
|
||||
# commands_impl/catalog_config._read. Without this, a file written by a
|
||||
# newer/incompatible Spec Kit was silently parsed under v1 assumptions on
|
||||
# the resolution path (bundle search/install), while the other reader
|
||||
# rejected it — the two readers disagreed. An absent schema_version stays
|
||||
# valid (backward compatible with configs that omit it).
|
||||
schema_version = data.get("schema_version")
|
||||
if schema_version is not None and (
|
||||
str(schema_version).strip().split(".")[0]
|
||||
!= CONFIG_SCHEMA_VERSION.split(".")[0]
|
||||
):
|
||||
raise BundlerError(
|
||||
f"Unsupported catalog config schema version "
|
||||
f"'{str(schema_version).strip()}' at {config_path}; this Spec Kit "
|
||||
f"understands version {CONFIG_SCHEMA_VERSION}. The file may have been "
|
||||
"written by a newer version or is corrupt."
|
||||
)
|
||||
catalogs = data.get("catalogs")
|
||||
if catalogs is None:
|
||||
catalogs = data.get("catalogs") if isinstance(data, dict) else None
|
||||
if not catalogs:
|
||||
return
|
||||
if not isinstance(catalogs, list):
|
||||
# Treat only an absent/``None`` ``catalogs`` as "nothing to merge"; any
|
||||
# other non-list value (``catalogs: 5``, ``false``, ``0``, ``''``,
|
||||
# ``{}``) is a malformed config and must raise, not be silently skipped
|
||||
# by a falsy check. Otherwise a truthy scalar would raise a raw
|
||||
# ``TypeError: 'int' object is not iterable`` from the loop below, while
|
||||
# falsy non-lists would be swallowed. Report the same actionable
|
||||
# BundlerError the sibling reader of this file raises
|
||||
# (commands_impl/catalog_config.py) so both readers of
|
||||
# bundle-catalogs.yml agree. An empty list stays valid (loop is a no-op).
|
||||
raise BundlerError(
|
||||
f"Malformed catalog config at {config_path}: 'catalogs' must be a "
|
||||
f"list, got {type(catalogs).__name__}."
|
||||
)
|
||||
for raw in catalogs:
|
||||
src = CatalogSource.from_dict(raw, scope)
|
||||
by_id[src.id] = src
|
||||
|
||||
@@ -96,46 +96,37 @@ class BundleManifest:
|
||||
if not isinstance(data, dict):
|
||||
raise BundlerError("Manifest must be a YAML mapping at the top level.")
|
||||
|
||||
schema_version = _text(data.get("schema_version"))
|
||||
schema_version = str(data.get("schema_version", "")).strip()
|
||||
|
||||
bundle_raw = data.get("bundle")
|
||||
if not isinstance(bundle_raw, dict):
|
||||
raise BundlerError("Manifest is missing the required 'bundle' mapping.")
|
||||
meta = BundleMeta(
|
||||
id=_text(bundle_raw.get("id")),
|
||||
name=_text(bundle_raw.get("name")),
|
||||
version=_text(bundle_raw.get("version")),
|
||||
role=_text(bundle_raw.get("role")),
|
||||
description=_text(bundle_raw.get("description")),
|
||||
author=_text(bundle_raw.get("author")),
|
||||
license=_text(bundle_raw.get("license")),
|
||||
id=str(bundle_raw.get("id", "")).strip(),
|
||||
name=str(bundle_raw.get("name", "")).strip(),
|
||||
version=str(bundle_raw.get("version", "")).strip(),
|
||||
role=str(bundle_raw.get("role", "")).strip(),
|
||||
description=str(bundle_raw.get("description", "")).strip(),
|
||||
author=str(bundle_raw.get("author", "")).strip(),
|
||||
license=str(bundle_raw.get("license", "")).strip(),
|
||||
)
|
||||
|
||||
requires_raw = data.get("requires")
|
||||
if requires_raw is None:
|
||||
requires_raw = {}
|
||||
elif not isinstance(requires_raw, dict):
|
||||
requires_raw = data.get("requires") or {}
|
||||
if not isinstance(requires_raw, dict):
|
||||
raise BundlerError("'requires' must be a mapping when present.")
|
||||
requires = Requires(
|
||||
speckit_version=_text(requires_raw.get("speckit_version")),
|
||||
speckit_version=str(requires_raw.get("speckit_version", "")).strip(),
|
||||
tools=_parse_str_list(requires_raw.get("tools"), "requires.tools"),
|
||||
mcp=_parse_str_list(requires_raw.get("mcp"), "requires.mcp"),
|
||||
)
|
||||
|
||||
integration = None
|
||||
integration_raw = data.get("integration")
|
||||
# Mirror the requires/provides guards above: a present-but-non-mapping
|
||||
# 'integration' (e.g. a bare string "copilot") was silently dropped,
|
||||
# leaving the bundle wrongly integration-agnostic. Reject it instead.
|
||||
if integration_raw is not None and not isinstance(integration_raw, dict):
|
||||
raise BundlerError("'integration' must be a mapping when present.")
|
||||
if isinstance(integration_raw, dict) and integration_raw.get("id"):
|
||||
integration = IntegrationRef(id=str(integration_raw["id"]).strip())
|
||||
|
||||
provides = data.get("provides")
|
||||
if provides is None:
|
||||
provides = {}
|
||||
elif not isinstance(provides, dict):
|
||||
provides = data.get("provides") or {}
|
||||
if not isinstance(provides, dict):
|
||||
raise BundlerError("'provides' must be a mapping when present.")
|
||||
|
||||
tags_raw = data.get("tags")
|
||||
@@ -220,22 +211,6 @@ class BundleManifest:
|
||||
return self.integration is None
|
||||
|
||||
|
||||
def _text(raw: Any) -> str:
|
||||
"""Coerce a manifest scalar into stripped text, mapping an explicit null to ``""``.
|
||||
|
||||
A ``.get(key, "")`` default only covers a *missing* key. A key that is
|
||||
present but null -- how YAML spells an empty field (``author:`` with nothing
|
||||
after it) -- yields ``None``, and ``str(None)`` is the literal ``"None"``.
|
||||
That text is non-empty, so it sailed past the ``if not value`` required-field
|
||||
checks in :meth:`BundleManifest.structural_errors`: an empty required field
|
||||
was silently accepted and the bundle shipped ``"None"`` as its
|
||||
author/license/description.
|
||||
"""
|
||||
if raw is None:
|
||||
return ""
|
||||
return str(raw).strip()
|
||||
|
||||
|
||||
def _parse_str_list(raw: Any, field_name: str) -> tuple[str, ...]:
|
||||
"""Coerce a manifest list-of-strings field into a tuple of strings.
|
||||
|
||||
@@ -263,7 +238,7 @@ def _parse_refs(kind: str, raw: Any) -> list[ComponentRef]:
|
||||
refs.append(
|
||||
ComponentRef(
|
||||
kind=kind,
|
||||
id=_text(item.get("id")),
|
||||
id=str(item.get("id", "")).strip(),
|
||||
version=(str(item["version"]).strip() if item.get("version") else None),
|
||||
source=(str(item["source"]).strip() if item.get("source") else None),
|
||||
priority=priority,
|
||||
|
||||
@@ -55,13 +55,8 @@ class InstalledBundleRecord:
|
||||
def from_dict(cls, data: Any) -> "InstalledBundleRecord":
|
||||
if not isinstance(data, dict):
|
||||
raise BundlerError("Each installed-bundle record must be a mapping.")
|
||||
components_raw = data.get("contributed_components")
|
||||
if components_raw is None:
|
||||
components_raw = []
|
||||
elif not isinstance(components_raw, list):
|
||||
# `or []` would coerce a FALSY non-list (0, '', False, {}) to []
|
||||
# before this guard, silently accepting a corrupt record; only an
|
||||
# absent/None value means "no components".
|
||||
components_raw = data.get("contributed_components") or []
|
||||
if not isinstance(components_raw, list):
|
||||
raise BundlerError(
|
||||
"Corrupt record: 'contributed_components' must be a list."
|
||||
)
|
||||
@@ -126,13 +121,8 @@ def load_records(project_root: Path) -> list[InstalledBundleRecord]:
|
||||
if not isinstance(data, dict):
|
||||
raise BundlerError(f"Corrupt records file: {path}")
|
||||
_check_schema_version(data.get("schema_version"), path=path, required=True)
|
||||
bundles = data.get("bundles")
|
||||
if bundles is None:
|
||||
bundles = []
|
||||
elif not isinstance(bundles, list):
|
||||
# `or []` would coerce a FALSY non-list (0, '', False, {}) to [] before
|
||||
# this guard, silently treating a corrupt file as "no bundles"; only an
|
||||
# absent/None value means empty.
|
||||
bundles = data.get("bundles") or []
|
||||
if not isinstance(bundles, list):
|
||||
raise BundlerError(
|
||||
f"Corrupt records file: {path} — 'bundles' must be a list."
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@ from urllib.parse import ParseResult, urlparse
|
||||
from urllib.request import url2pathname
|
||||
|
||||
from ..._assets import _locate_core_pack, _repo_root
|
||||
from ..._download_security import MAX_JSON_CATALOG_BYTES, read_response_limited
|
||||
from .. import BundlerError
|
||||
from ..lib.yamlio import loads_json
|
||||
from ..models.catalog import CatalogSource
|
||||
@@ -77,8 +76,6 @@ def _validate_remote_url(source_id: str, url: str) -> None:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
# Accessing ``port`` performs urllib's syntax/range validation.
|
||||
_ = parsed.port
|
||||
except ValueError:
|
||||
raise BundlerError(
|
||||
f"Catalog '{source_id}' URL is malformed: {url}"
|
||||
@@ -120,15 +117,7 @@ def make_catalog_fetcher(*, allow_network: bool = True):
|
||||
|
||||
def fetch(source: CatalogSource) -> dict:
|
||||
url = source.url
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
# Keep malformed authorities and ports inside the BundlerError
|
||||
# contract even when a config file was edited by hand.
|
||||
_ = parsed.port
|
||||
except ValueError:
|
||||
raise BundlerError(
|
||||
f"Catalog {source.id!r} URL is malformed: {url!r}"
|
||||
) from None
|
||||
parsed = urlparse(url)
|
||||
scheme = parsed.scheme.lower()
|
||||
|
||||
if scheme == "builtin":
|
||||
@@ -191,12 +180,7 @@ def _http_get_json(source_id: str, url: str) -> dict:
|
||||
) as response:
|
||||
final_url = response.geturl()
|
||||
_validate_remote_url(source_id, final_url)
|
||||
raw = read_response_limited(
|
||||
response,
|
||||
max_bytes=MAX_JSON_CATALOG_BYTES,
|
||||
error_type=BundlerError,
|
||||
label=f"bundle catalog '{source_id}'",
|
||||
).decode("utf-8")
|
||||
raw = response.read().decode("utf-8")
|
||||
except BundlerError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -227,10 +211,6 @@ class DefaultPrimitiveInstaller:
|
||||
manager = self._manager_for(component, project_root)
|
||||
manager.install(component)
|
||||
|
||||
def refresh(self, project_root: Path, component: ComponentRef) -> None:
|
||||
manager = self._manager_for(component, project_root)
|
||||
manager.refresh(component)
|
||||
|
||||
def remove(self, project_root: Path, component: ComponentRef) -> None:
|
||||
manager = self._manager_for(component, project_root)
|
||||
manager.remove(component)
|
||||
|
||||
@@ -50,10 +50,7 @@ class InstallResult:
|
||||
|
||||
@property
|
||||
def changed(self) -> bool:
|
||||
# `uninstalled` is a mutating outcome too: a `bundle update` whose new
|
||||
# manifest drops components (removing them via the refresh path) with no
|
||||
# new install/refresh must still report changed=True, not a no-op.
|
||||
return bool(self.installed or self.refreshed or self.uninstalled)
|
||||
return bool(self.installed or self.refreshed)
|
||||
|
||||
|
||||
def install_bundle(
|
||||
|
||||
@@ -142,10 +142,4 @@ def _collect_files(
|
||||
# Skip symlinked files to avoid escaping the bundle directory.
|
||||
continue
|
||||
collected.append(path)
|
||||
# Order by the canonical POSIX arcname (the same key build_bundle uses to
|
||||
# NAME each member), not by pathlib.Path comparison. Path ordering is
|
||||
# platform-dependent (Windows folds case and uses backslash separators),
|
||||
# which would lay out zip members differently across build hosts and break
|
||||
# the byte-for-byte reproducible-build guarantee even though the member
|
||||
# names are identical.
|
||||
return sorted(collected, key=lambda p: p.relative_to(bundle_dir).as_posix())
|
||||
return sorted(collected)
|
||||
|
||||
@@ -85,17 +85,11 @@ def _bundled_manifest_version(manifest_path: Path, root_key: str) -> str | None:
|
||||
|
||||
|
||||
class _KindManager(Protocol):
|
||||
def is_installed(self, component: ComponentRef) -> bool:
|
||||
pass
|
||||
def is_installed(self, component: ComponentRef) -> bool: ...
|
||||
|
||||
def install(self, component: ComponentRef) -> None:
|
||||
pass
|
||||
def install(self, component: ComponentRef) -> None: ...
|
||||
|
||||
def refresh(self, component: ComponentRef) -> None:
|
||||
pass
|
||||
|
||||
def remove(self, component: ComponentRef) -> None:
|
||||
pass
|
||||
def remove(self, component: ComponentRef) -> None: ...
|
||||
|
||||
|
||||
def primitive_manager(
|
||||
@@ -157,12 +151,6 @@ class _PresetKindManager:
|
||||
return False
|
||||
|
||||
def install(self, component: ComponentRef) -> None:
|
||||
self._do_install(component, force=False)
|
||||
|
||||
def refresh(self, component: ComponentRef) -> None:
|
||||
self._do_install(component, force=True)
|
||||
|
||||
def _do_install(self, component: ComponentRef, *, force: bool) -> None:
|
||||
from ... import get_speckit_version
|
||||
from ..._assets import _locate_bundled_preset
|
||||
|
||||
@@ -180,9 +168,7 @@ class _PresetKindManager:
|
||||
component.version,
|
||||
_bundled_manifest_version(bundled / "preset.yml", "preset"),
|
||||
)
|
||||
self._manager.install_from_directory(
|
||||
bundled, speckit_version, priority, **({"force": True} if force else {})
|
||||
)
|
||||
self._manager.install_from_directory(bundled, speckit_version, priority)
|
||||
return
|
||||
|
||||
if not self._allow_network:
|
||||
@@ -208,9 +194,7 @@ class _PresetKindManager:
|
||||
)
|
||||
zip_path = catalog.download_pack(component.id)
|
||||
try:
|
||||
self._manager.install_from_zip(
|
||||
zip_path, speckit_version, priority, **({"force": True} if force else {})
|
||||
)
|
||||
self._manager.install_from_zip(zip_path, speckit_version, priority)
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
if zip_path.exists():
|
||||
@@ -240,12 +224,6 @@ class _ExtensionKindManager:
|
||||
return False
|
||||
|
||||
def install(self, component: ComponentRef) -> None:
|
||||
self._do_install(component, force=False)
|
||||
|
||||
def refresh(self, component: ComponentRef) -> None:
|
||||
self._do_install(component, force=True)
|
||||
|
||||
def _do_install(self, component: ComponentRef, *, force: bool) -> None:
|
||||
from ... import get_speckit_version
|
||||
from ..._assets import _locate_bundled_extension
|
||||
|
||||
@@ -264,7 +242,7 @@ class _ExtensionKindManager:
|
||||
_bundled_manifest_version(bundled / "extension.yml", "extension"),
|
||||
)
|
||||
self._manager.install_from_directory(
|
||||
bundled, speckit_version, priority=priority, force=force
|
||||
bundled, speckit_version, priority=priority
|
||||
)
|
||||
return
|
||||
|
||||
@@ -294,7 +272,7 @@ class _ExtensionKindManager:
|
||||
zip_path = catalog.download_extension(component.id)
|
||||
try:
|
||||
self._manager.install_from_zip(
|
||||
zip_path, speckit_version, priority=priority, force=force
|
||||
zip_path, speckit_version, priority=priority
|
||||
)
|
||||
finally:
|
||||
with contextlib.suppress(Exception):
|
||||
@@ -340,11 +318,6 @@ class _WorkflowKindManager:
|
||||
lambda: workflow_add(component.id),
|
||||
)
|
||||
|
||||
def refresh(self, component: ComponentRef) -> None:
|
||||
# workflow_add is idempotent for already-installed workflows; delegate
|
||||
# to the standard install path which handles version refresh correctly.
|
||||
self.install(component)
|
||||
|
||||
def _assert_pinned_version(self, component: ComponentRef) -> None:
|
||||
if not component.version:
|
||||
return
|
||||
@@ -405,35 +378,6 @@ class _StepKindManager:
|
||||
lambda: workflow_step_add(component.id),
|
||||
)
|
||||
|
||||
def refresh(self, component: ComponentRef) -> None:
|
||||
# Preserve an existing step until we've validated we can perform refresh.
|
||||
# For already-installed steps, keep a backup and restore it if the
|
||||
# remove+reinstall path fails.
|
||||
if not (self._allow_network and self.is_installed(component)):
|
||||
self.install(component)
|
||||
return
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
step_dir = self._registry.steps_dir / component.id
|
||||
metadata = self._registry.get(component.id)
|
||||
backup_dir = Path(tempfile.mkdtemp(prefix="speckit-step-refresh-")) / component.id
|
||||
try:
|
||||
if step_dir.exists():
|
||||
shutil.copytree(step_dir, backup_dir)
|
||||
self.remove(component)
|
||||
try:
|
||||
self.install(component)
|
||||
except BundlerError:
|
||||
if backup_dir.exists():
|
||||
shutil.copytree(backup_dir, step_dir, dirs_exist_ok=True)
|
||||
if metadata is not None and not self._registry.is_installed(component.id):
|
||||
self._registry.add(component.id, metadata)
|
||||
raise
|
||||
finally:
|
||||
shutil.rmtree(backup_dir.parent, ignore_errors=True)
|
||||
|
||||
def remove(self, component: ComponentRef) -> None:
|
||||
from ... import workflow_step_remove
|
||||
|
||||
|
||||
@@ -74,13 +74,6 @@ class CatalogStackBase:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
# Accessing ``port`` performs urllib's syntax/range validation;
|
||||
# ``hostname`` alone does not, so a non-numeric or out-of-range
|
||||
# port would otherwise pass validation here and only fail later,
|
||||
# at fetch time, as an error this module does not translate --
|
||||
# a raw http.client.InvalidURL for a non-numeric port, and a
|
||||
# socket-layer failure for one that is merely out of range.
|
||||
_ = parsed.port
|
||||
except ValueError:
|
||||
raise cls._error(f"Catalog URL is malformed: {url}") from None
|
||||
is_localhost = hostname in ("localhost", "127.0.0.1", "::1")
|
||||
|
||||
@@ -12,10 +12,8 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from rich.markup import escape as _escape_markup
|
||||
|
||||
from ..._console import console, err_console
|
||||
from ..._download_security import MAX_DOWNLOAD_BYTES, read_response_limited
|
||||
from ...bundler import BundlerError
|
||||
from ...bundler.lib.project import (
|
||||
active_integration,
|
||||
@@ -186,16 +184,11 @@ def bundle_search(
|
||||
else ""
|
||||
)
|
||||
console.print(
|
||||
f" [bold]{_escape_markup(str(r.entry.id))}[/bold] "
|
||||
f"v{_escape_markup(str(r.entry.version))} — "
|
||||
f"{_escape_markup(str(r.entry.name))} "
|
||||
f"[dim]({_escape_markup(str(r.entry.role))})[/dim] "
|
||||
f"{_trust_badge(r.entry.verified)} {policy}"
|
||||
)
|
||||
console.print(f" {_escape_markup(str(r.entry.description))}")
|
||||
console.print(
|
||||
f" [dim]source: {_escape_markup(str(r.source.id))}[/dim]"
|
||||
f" [bold]{r.entry.id}[/bold] v{r.entry.version} — {r.entry.name} "
|
||||
f"[dim]({r.entry.role})[/dim] {_trust_badge(r.entry.verified)} {policy}"
|
||||
)
|
||||
console.print(f" {r.entry.description}")
|
||||
console.print(f" [dim]source: {r.source.id}[/dim]")
|
||||
|
||||
|
||||
@bundle_app.command("info")
|
||||
@@ -248,31 +241,16 @@ def bundle_info(
|
||||
print(_json.dumps(payload, indent=2))
|
||||
return
|
||||
|
||||
console.print(
|
||||
f"\n[bold cyan]{_escape_markup(str(entry.id))}[/bold cyan] "
|
||||
f"v{_escape_markup(str(entry.version))} — "
|
||||
f"{_escape_markup(str(entry.name))}"
|
||||
)
|
||||
console.print(f" Role: {_escape_markup(str(entry.role))}")
|
||||
console.print(f" {_escape_markup(str(entry.description))}")
|
||||
console.print(
|
||||
f" Author: {_escape_markup(str(entry.author))} "
|
||||
f"License: {_escape_markup(str(entry.license))}"
|
||||
)
|
||||
console.print(
|
||||
f" Source: {_escape_markup(str(resolved.source.id))} "
|
||||
f"({resolved.source.install_policy.value})"
|
||||
)
|
||||
console.print(f"\n[bold cyan]{entry.id}[/bold cyan] v{entry.version} — {entry.name}")
|
||||
console.print(f" Role: {entry.role}")
|
||||
console.print(f" {entry.description}")
|
||||
console.print(f" Author: {entry.author} License: {entry.license}")
|
||||
console.print(f" Source: {resolved.source.id} ({resolved.source.install_policy.value})")
|
||||
console.print(f" Trust: {_trust_badge(entry.verified)}")
|
||||
if entry.requires_speckit_version:
|
||||
console.print(
|
||||
f" Requires Spec Kit: "
|
||||
f"{_escape_markup(str(entry.requires_speckit_version))}"
|
||||
)
|
||||
console.print(f" Requires Spec Kit: {entry.requires_speckit_version}")
|
||||
if manifest and manifest.integration:
|
||||
console.print(
|
||||
f" Integration: {_escape_markup(str(manifest.integration.id))}"
|
||||
)
|
||||
console.print(f" Integration: {manifest.integration.id}")
|
||||
|
||||
if components:
|
||||
console.print("\n [bold]Components[/bold] (added on install):")
|
||||
@@ -282,22 +260,18 @@ def bundle_info(
|
||||
continue
|
||||
console.print(f" [bold]{kind}:[/bold]")
|
||||
for item in items:
|
||||
console.print(
|
||||
f" - {_escape_markup(_format_component(item))}"
|
||||
)
|
||||
console.print(f" - {_format_component(item)}")
|
||||
else:
|
||||
console.print("\n [bold]Provides:[/bold]")
|
||||
for kind in ("extensions", "presets", "steps", "workflows"):
|
||||
count = entry.provides.get(kind, 0)
|
||||
if count:
|
||||
console.print(f" {kind}: {_escape_markup(str(count))}")
|
||||
console.print(f" {kind}: {count}")
|
||||
|
||||
if overlaps:
|
||||
console.print("\n [yellow]Overlaps with already-installed bundles:[/yellow]")
|
||||
for overlap in overlaps:
|
||||
console.print(
|
||||
f" [yellow]-[/yellow] {_escape_markup(str(overlap))}"
|
||||
)
|
||||
console.print(f" [yellow]-[/yellow] {overlap}")
|
||||
|
||||
if not resolved.install_allowed:
|
||||
console.print(
|
||||
@@ -363,10 +337,6 @@ def bundle_install(
|
||||
local_manifest = _local_manifest_source(bundle_id)
|
||||
if local_manifest is not None:
|
||||
manifest = local_manifest
|
||||
_validate_manifest_structure(
|
||||
manifest,
|
||||
source=f"Local bundle source {bundle_id!r}",
|
||||
)
|
||||
else:
|
||||
stack = _build_stack(project_root or Path.cwd(), offline=offline)
|
||||
resolved = stack.resolve(bundle_id)
|
||||
@@ -380,16 +350,6 @@ def bundle_install(
|
||||
|
||||
if project_root is None:
|
||||
init_integration = _resolve_init_integration(integration, manifest)
|
||||
# Resolve all hard compatibility gates before ``specify init``.
|
||||
# Otherwise an incompatible but structurally valid bundle would
|
||||
# initialize a project and only then fail its version/integration
|
||||
# checks, leaving state behind after a failed install.
|
||||
resolve_install_plan(
|
||||
manifest,
|
||||
speckit_version=_speckit_version(),
|
||||
active_integration=init_integration,
|
||||
integration_explicit=True,
|
||||
)
|
||||
console.print(
|
||||
f"[cyan]No Spec Kit project here; initializing with integration "
|
||||
f"'{init_integration}'…[/cyan]"
|
||||
@@ -751,24 +711,17 @@ def _local_manifest_source(arg: str):
|
||||
|
||||
if candidate.suffix == ".zip":
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
import yaml as _yaml
|
||||
|
||||
from ..._download_security import open_zip_bounded, read_zip_member_limited
|
||||
|
||||
with open_zip_bounded(candidate, error_type=BundlerError) as archive:
|
||||
with zipfile.ZipFile(candidate) as archive:
|
||||
try:
|
||||
archive.getinfo("bundle.yml")
|
||||
raw = archive.read("bundle.yml")
|
||||
except KeyError as exc:
|
||||
raise BundlerError(
|
||||
f"Artifact '{candidate}' does not contain a bundle.yml."
|
||||
) from exc
|
||||
raw = read_zip_member_limited(
|
||||
archive,
|
||||
"bundle.yml",
|
||||
error_type=BundlerError,
|
||||
label="bundle manifest",
|
||||
)
|
||||
data = _yaml.safe_load(io.BytesIO(raw))
|
||||
return BundleManifest.from_dict(data)
|
||||
|
||||
@@ -852,13 +805,7 @@ def _download_manifest(resolved, *, offline: bool):
|
||||
f"Network access disabled; cannot download bundle '{resolved.entry.id}' "
|
||||
f"from {url}."
|
||||
)
|
||||
manifest = _download_remote_manifest(
|
||||
resolved.entry.id,
|
||||
url,
|
||||
expected_sha256=getattr(resolved.entry, "sha256", None),
|
||||
)
|
||||
_validate_catalog_manifest(resolved.entry, manifest)
|
||||
return manifest
|
||||
return _download_remote_manifest(resolved.entry.id, url)
|
||||
|
||||
|
||||
def _require_https(label: str, url: str) -> None:
|
||||
@@ -870,8 +817,6 @@ def _require_https(label: str, url: str) -> None:
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
hostname = parsed.hostname
|
||||
# Accessing ``port`` performs urllib's syntax/range validation.
|
||||
_ = parsed.port
|
||||
except ValueError:
|
||||
raise BundlerError(
|
||||
f"Refusing to download {label}: URL is malformed: {url}"
|
||||
@@ -885,12 +830,7 @@ def _require_https(label: str, url: str) -> None:
|
||||
raise BundlerError(f"Refusing to download {label} from URL with no host: {url}")
|
||||
|
||||
|
||||
def _download_remote_manifest(
|
||||
entry_id: str,
|
||||
url: str,
|
||||
*,
|
||||
expected_sha256: str | None = None,
|
||||
):
|
||||
def _download_remote_manifest(entry_id: str, url: str):
|
||||
"""Fetch a remote bundle artifact over HTTPS and extract its manifest."""
|
||||
import io
|
||||
import tempfile
|
||||
@@ -902,7 +842,6 @@ def _download_remote_manifest(
|
||||
from ...authentication.http import github_provider_hosts, open_url
|
||||
from ..._github_http import resolve_github_release_asset_api_url
|
||||
from ...bundler.models.manifest import BundleManifest
|
||||
from ...shared_infra import verify_archive_sha256
|
||||
|
||||
def _validate_redirect(old_url: str, new_url: str) -> None:
|
||||
_require_https(f"bundle '{entry_id}'", new_url)
|
||||
@@ -940,18 +879,7 @@ def _download_remote_manifest(
|
||||
extra_headers=extra_headers,
|
||||
) as resp:
|
||||
_require_https(f"bundle '{entry_id}'", resp.geturl())
|
||||
raw = read_response_limited(
|
||||
resp,
|
||||
max_bytes=MAX_DOWNLOAD_BYTES,
|
||||
error_type=BundlerError,
|
||||
label=f"bundle '{entry_id}' download",
|
||||
)
|
||||
verify_archive_sha256(
|
||||
raw,
|
||||
expected_sha256,
|
||||
entry_id,
|
||||
BundlerError,
|
||||
)
|
||||
raw = resp.read()
|
||||
except BundlerError:
|
||||
raise
|
||||
except Exception as exc: # noqa: BLE001
|
||||
@@ -1012,38 +940,6 @@ def _download_remote_manifest(
|
||||
) from exc
|
||||
|
||||
|
||||
def _validate_manifest_structure(manifest, *, source: str) -> None:
|
||||
"""Reject a malformed manifest before any project mutation can occur."""
|
||||
from ...bundler.services.validator import validate_manifest
|
||||
|
||||
report = validate_manifest(manifest)
|
||||
if report.ok:
|
||||
return
|
||||
raise BundlerError(
|
||||
f"{source} contains an invalid bundle manifest:\n - "
|
||||
+ "\n - ".join(report.errors)
|
||||
)
|
||||
|
||||
|
||||
def _validate_catalog_manifest(entry, manifest) -> None:
|
||||
"""Bind a downloaded manifest to the catalog identity that selected it."""
|
||||
if manifest.bundle.id != entry.id:
|
||||
raise BundlerError(
|
||||
f"Downloaded bundle id mismatch: catalog entry {entry.id!r} points to "
|
||||
f"a manifest for {manifest.bundle.id!r}."
|
||||
)
|
||||
if manifest.bundle.version != entry.version:
|
||||
raise BundlerError(
|
||||
f"Downloaded bundle version mismatch for {entry.id!r}: catalog declares "
|
||||
f"{entry.version!r}, but the manifest declares "
|
||||
f"{manifest.bundle.version!r}."
|
||||
)
|
||||
_validate_manifest_structure(
|
||||
manifest,
|
||||
source=f"Downloaded bundle {entry.id!r}",
|
||||
)
|
||||
|
||||
|
||||
def register(app: typer.Typer) -> None:
|
||||
"""Attach the bundle command group to the root Typer app."""
|
||||
app.add_typer(bundle_app, name="bundle")
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
"""specify event * command handlers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import typer
|
||||
|
||||
event_app = typer.Typer(
|
||||
name="event",
|
||||
help="Manage and execute event-driven commands",
|
||||
add_completion=False,
|
||||
)
|
||||
|
||||
|
||||
@event_app.command("run")
|
||||
def event_run(
|
||||
command_name: str = typer.Argument(..., help="Name of the command to execute"),
|
||||
event_name: str = typer.Argument(..., help="Canonical event name (e.g., session_start)"),
|
||||
timeout: int = typer.Argument(
|
||||
120, help="Per-handler timeout in seconds (passed through from the native hook config)"
|
||||
),
|
||||
):
|
||||
"""Resolve and run an event-driven command script with stdin payload."""
|
||||
from ..events import resolve_and_run_event_command
|
||||
|
||||
# Read payload from stdin if available
|
||||
payload = sys.stdin.read() if not sys.stdin.isatty() else "{}"
|
||||
|
||||
# Run the event command
|
||||
project_root = Path.cwd() # The agent runs events from project root
|
||||
exit_code = resolve_and_run_event_command(
|
||||
command_name, event_name, payload, project_root, timeout=timeout
|
||||
)
|
||||
raise typer.Exit(code=exit_code)
|
||||
|
||||
|
||||
def register(app: typer.Typer) -> None:
|
||||
app.add_typer(event_app, name="event")
|
||||
@@ -183,7 +183,6 @@ def register(app: typer.Typer) -> None:
|
||||
save_init_options,
|
||||
)
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
)
|
||||
from ..integrations._commands import (
|
||||
@@ -443,20 +442,12 @@ def register(app: typer.Typer) -> None:
|
||||
if extra:
|
||||
integration_parsed_options.update(extra)
|
||||
|
||||
from ..events import resolve_events
|
||||
events_map = resolve_events(
|
||||
resolved_integration.key,
|
||||
resolved_integration.config,
|
||||
project_path,
|
||||
integration_parsed_options or None,
|
||||
)
|
||||
resolved_integration.setup(
|
||||
project_path,
|
||||
manifest,
|
||||
parsed_options=integration_parsed_options or None,
|
||||
script_type=selected_script,
|
||||
raw_options=integration_options,
|
||||
events=events_map,
|
||||
)
|
||||
manifest.save()
|
||||
|
||||
@@ -490,12 +481,6 @@ def register(app: typer.Typer) -> None:
|
||||
invoke_separator=resolved_integration.effective_invoke_separator(
|
||||
integration_parsed_options, project_root=project_path
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
resolved_integration,
|
||||
resolved_integration.key,
|
||||
integration_parsed_options,
|
||||
project_path,
|
||||
),
|
||||
)
|
||||
tracker.complete(
|
||||
"shared-infra", f"scripts ({selected_script}) + templates"
|
||||
@@ -715,7 +700,6 @@ def register(app: typer.Typer) -> None:
|
||||
zed_skill_mode = selected_ai == "zed" and _is_skills_integration
|
||||
grok_skill_mode = selected_ai == "grok" and _is_skills_integration
|
||||
cline_skill_mode = selected_ai == "cline"
|
||||
forge_skill_mode = selected_ai == "forge"
|
||||
bob_skill_mode = selected_ai == "bob" and _is_skills_integration
|
||||
native_skill_mode = (
|
||||
codex_skill_mode
|
||||
@@ -792,7 +776,6 @@ def register(app: typer.Typer) -> None:
|
||||
if (
|
||||
_is_slash_skills_agent(selected_ai, _ai_skills_enabled)
|
||||
or cline_skill_mode
|
||||
or forge_skill_mode
|
||||
):
|
||||
return f"/speckit-{name}"
|
||||
return f"/speckit.{name}"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from ._invocation_style import get_invocation_prefix
|
||||
from .integration_state import integration_setting, integration_settings
|
||||
|
||||
|
||||
@@ -65,15 +64,8 @@ def with_integration_setting(
|
||||
elif raw_options is not None:
|
||||
current.pop("parsed_options", None)
|
||||
|
||||
# Recompute the separator from the options actually STORED on ``current``
|
||||
# after the update, not the raw ``parsed_options`` argument. When only
|
||||
# ``script_type`` changes (``parsed_options`` and ``raw_options`` both
|
||||
# None), the previously-stored ``parsed_options`` are retained above, so
|
||||
# deriving the separator from the argument (None) would drop an
|
||||
# options-dependent separator (e.g. Copilot ``--skills`` -> "-") back to
|
||||
# the default ".".
|
||||
current["invoke_separator"] = integration.effective_invoke_separator(
|
||||
current.get("parsed_options"), project_root
|
||||
parsed_options, project_root
|
||||
)
|
||||
settings[key] = current
|
||||
return settings
|
||||
@@ -100,14 +92,3 @@ def invoke_separator_for_integration(
|
||||
return integration.effective_invoke_separator(stored_parsed, project_root)
|
||||
|
||||
return integration.effective_invoke_separator(None, project_root)
|
||||
|
||||
|
||||
def invoke_prefix_for_integration(
|
||||
integration: Any,
|
||||
key: str,
|
||||
parsed_options: dict[str, Any] | None = None,
|
||||
project_root: Any = None,
|
||||
) -> str:
|
||||
"""Resolve the native invocation prefix for an integration's output mode."""
|
||||
skills_mode = integration.is_skills_mode(parsed_options, project_root)
|
||||
return get_invocation_prefix(key, skills_mode)
|
||||
|
||||
@@ -48,7 +48,6 @@ def _register_builtins() -> None:
|
||||
"""
|
||||
# -- Imports (alphabetical) -------------------------------------------
|
||||
from .agy import AgyIntegration
|
||||
from .alquimia import AlquimiaAIIntegration
|
||||
from .amp import AmpIntegration
|
||||
from .auggie import AuggieIntegration
|
||||
from .bob import BobIntegration
|
||||
@@ -59,7 +58,6 @@ def _register_builtins() -> None:
|
||||
from .copilot import CopilotIntegration
|
||||
from .cursor_agent import CursorAgentIntegration
|
||||
from .devin import DevinIntegration
|
||||
from .droid import DroidIntegration
|
||||
from .firebender import FirebenderIntegration
|
||||
from .forge import ForgeIntegration
|
||||
from .gemini import GeminiIntegration
|
||||
@@ -87,7 +85,6 @@ def _register_builtins() -> None:
|
||||
|
||||
# -- Registration (alphabetical) --------------------------------------
|
||||
_register(AgyIntegration())
|
||||
_register(AlquimiaAIIntegration())
|
||||
_register(AmpIntegration())
|
||||
_register(AuggieIntegration())
|
||||
_register(BobIntegration())
|
||||
@@ -98,7 +95,6 @@ def _register_builtins() -> None:
|
||||
_register(CopilotIntegration())
|
||||
_register(CursorAgentIntegration())
|
||||
_register(DevinIntegration())
|
||||
_register(DroidIntegration())
|
||||
_register(FirebenderIntegration())
|
||||
_register(ForgeIntegration())
|
||||
_register(GeminiIntegration())
|
||||
|
||||
@@ -11,7 +11,6 @@ from rich.markup import escape
|
||||
from .._agent_config import SCRIPT_TYPE_CHOICES
|
||||
from .._console import console
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
invoke_separator_for_integration as _invoke_separator_for_integration,
|
||||
resolve_integration_options as _resolve_integration_options_impl,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
@@ -121,7 +120,8 @@ def _clear_init_options_for_integration(project_root: Path, integration_key: str
|
||||
def _remove_integration_json(project_root: Path) -> None:
|
||||
"""Remove ``.specify/integration.json`` if it exists."""
|
||||
path = project_root / INTEGRATION_JSON
|
||||
path.unlink(missing_ok=True)
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -333,9 +333,6 @@ def _set_default_integration(
|
||||
integration, {"integration_settings": settings}, key, parsed_options,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
integration, key, parsed_options, project_root
|
||||
),
|
||||
force=refresh_templates_force,
|
||||
refresh_managed=True,
|
||||
refresh_hint=refresh_hint,
|
||||
@@ -394,24 +391,23 @@ def _register_extensions_for_agent(
|
||||
agent_key: str,
|
||||
*,
|
||||
continuing: str,
|
||||
force: bool = False,
|
||||
) -> None:
|
||||
"""Register all enabled extensions' commands/skills for ``agent_key``.
|
||||
|
||||
``use`` / ``switch`` re-register enabled extensions for the agent they
|
||||
activate (rescaffold); ``upgrade`` does so only for the *active*
|
||||
integration. Plain ``install`` and upgrade of a non-active integration
|
||||
deliberately skip this helper so a secondary integration has no extension
|
||||
side effects until it is selected. See issues #2886 and #2948.
|
||||
activate; ``upgrade`` backfills them for the refreshed agent. Plain
|
||||
``install`` deliberately does not call this helper so adding a secondary
|
||||
integration has no extension side effects until it is selected or upgraded.
|
||||
See issue #2886.
|
||||
|
||||
Callers always pass the active agent (use/switch activate the target
|
||||
before registering), so extension *skill* rendering — which is scoped to
|
||||
the active ``ai`` / ``ai_skills`` init-options — matches ``agent_key``.
|
||||
|
||||
When ``force=True``, existing skill files are overwritten even when they
|
||||
are not dev-mode symlinks. Pass ``force=True`` in the upgrade path so that
|
||||
extension content is layered on top of the core-template files that
|
||||
``setup()`` just regenerated (fixes the skip-guard bug for skills mode).
|
||||
Known limitation: extension *skill* rendering is scoped to the active
|
||||
agent (init-options track a single ``ai`` / ``ai_skills`` pair). A
|
||||
skills-mode agent registered while it is *not* the active agent (e.g.
|
||||
Copilot ``--skills`` registered while non-active) therefore
|
||||
receives command files rather than skills here — matching ``extension
|
||||
add``'s multi-agent behavior. ``use`` / ``switch`` avoid this because they
|
||||
make the target the active agent first. Per-agent skills parity is tracked in
|
||||
#2948.
|
||||
|
||||
Best-effort: never aborts the surrounding integration operation. Callers
|
||||
invoke it *after* the use/upgrade/switch transaction has committed so a
|
||||
@@ -420,7 +416,7 @@ def _register_extensions_for_agent(
|
||||
_best_effort_extension_op(
|
||||
project_root,
|
||||
agent_key,
|
||||
lambda mgr, key: mgr.register_enabled_extensions_for_agent(key, force=force),
|
||||
lambda mgr, key: mgr.register_enabled_extensions_for_agent(key),
|
||||
phase="register extension artifacts for",
|
||||
continuing=continuing,
|
||||
)
|
||||
@@ -447,91 +443,6 @@ def _unregister_extensions_for_agent(
|
||||
)
|
||||
|
||||
|
||||
def _register_presets_for_agent(
|
||||
project_root: Path,
|
||||
agent_key: str,
|
||||
*,
|
||||
continuing: str,
|
||||
) -> None:
|
||||
"""Register all enabled presets' command overrides/skills for ``agent_key``.
|
||||
|
||||
Presets follow the same single-active rule as extensions (#2948):
|
||||
``use`` / ``switch`` re-register enabled presets for the agent they
|
||||
activate (rescaffold), so a preset installed while a different
|
||||
integration was active is not left targeting that inactive integration.
|
||||
|
||||
Best-effort: never aborts the surrounding integration operation.
|
||||
"""
|
||||
try:
|
||||
from ..presets import PresetManager
|
||||
|
||||
preset_mgr = PresetManager(project_root)
|
||||
preset_mgr.register_enabled_presets_for_agent(agent_key)
|
||||
except Exception as preset_err:
|
||||
from .. import _print_cli_warning
|
||||
|
||||
_print_cli_warning(
|
||||
"register preset artifacts for",
|
||||
"integration",
|
||||
agent_key,
|
||||
preset_err,
|
||||
continuing=continuing,
|
||||
)
|
||||
|
||||
|
||||
def _unregister_presets_for_agent(
|
||||
project_root: Path,
|
||||
agent_key: str,
|
||||
*,
|
||||
continuing: str,
|
||||
) -> None:
|
||||
"""Best-effort removal of ``agent_key``'s preset command/skill artifacts.
|
||||
|
||||
Mirrors ``_unregister_extensions_for_agent``: used by ``switch`` when
|
||||
uninstalling the previous integration so its preset command overrides
|
||||
and skill mirrors don't linger as orphans in the old agent's directory
|
||||
once a different (possibly not-yet-installed) integration becomes
|
||||
active (#2948).
|
||||
|
||||
Best-effort: never aborts the surrounding integration operation.
|
||||
"""
|
||||
try:
|
||||
from ..presets import PresetManager
|
||||
|
||||
preset_mgr = PresetManager(project_root)
|
||||
preset_mgr.unregister_agent_artifacts(agent_key)
|
||||
except Exception as preset_err:
|
||||
from .. import _print_cli_warning
|
||||
|
||||
_print_cli_warning(
|
||||
"clean up preset artifacts for",
|
||||
"integration",
|
||||
agent_key,
|
||||
preset_err,
|
||||
continuing=continuing,
|
||||
)
|
||||
|
||||
|
||||
def _unregister_enabled_extension_commands_for_agent(
|
||||
project_root: Path,
|
||||
agent_key: str,
|
||||
*,
|
||||
continuing: str,
|
||||
) -> None:
|
||||
"""Best-effort removal of enabled extension command artifacts for ``agent_key``."""
|
||||
_best_effort_extension_op(
|
||||
project_root,
|
||||
agent_key,
|
||||
lambda mgr, key: mgr.unregister_agent_artifacts(
|
||||
key,
|
||||
enabled_only=True,
|
||||
commands_only=True,
|
||||
),
|
||||
phase="clean up enabled extension command artifacts for",
|
||||
continuing=continuing,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI formatting helpers (re-exported from _commands.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -8,7 +8,6 @@ import typer
|
||||
from .._console import console
|
||||
from .._utils import _display_project_path
|
||||
from ..integration_runtime import (
|
||||
invoke_prefix_for_integration as _invoke_prefix_for_integration,
|
||||
invoke_separator_for_integration as _invoke_separator_for_integration,
|
||||
with_integration_setting as _with_integration_setting,
|
||||
)
|
||||
@@ -131,9 +130,6 @@ def integration_install(
|
||||
infra_integration, current, infra_key, infra_parsed,
|
||||
project_root=project_root,
|
||||
),
|
||||
invoke_prefix=_invoke_prefix_for_integration(
|
||||
infra_integration, infra_key, infra_parsed, project_root
|
||||
),
|
||||
)
|
||||
if os.name != "nt":
|
||||
from .. import ensure_executable_scripts
|
||||
@@ -143,21 +139,12 @@ def integration_install(
|
||||
integration.key, project_root, version=_get_speckit_version()
|
||||
)
|
||||
|
||||
from ..events import resolve_events
|
||||
events_map = resolve_events(
|
||||
integration.key,
|
||||
integration.config,
|
||||
project_root,
|
||||
parsed_options,
|
||||
)
|
||||
|
||||
try:
|
||||
integration.setup(
|
||||
project_root, manifest,
|
||||
parsed_options=parsed_options,
|
||||
script_type=selected_script,
|
||||
raw_options=raw_options,
|
||||
events=events_map,
|
||||
)
|
||||
manifest.save()
|
||||
new_installed = _dedupe_integration_keys([*installed_keys, integration.key])
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user