mirror of
https://github.com/github/spec-kit.git
synced 2026-07-04 04:45:43 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3343967c3 |
36
.github/workflows/add-community-extension.lock.yml
generated
vendored
36
.github/workflows/add-community-extension.lock.yml
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"2ace61d3a4e86e81ce7ff110e118981b4d88a06aa351ecdc2c3b64e44b10690f","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f1073a236eb41f9fc2b5b8c1e58c25e02b5a6d18d242887636acc9007dd1542e","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"efa55847f72aadb03490d955263ff911bf758700","version":"v0.74.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
|
||||
# ___ _ _
|
||||
# / _ \ | | (_)
|
||||
@@ -204,23 +204,23 @@ jobs:
|
||||
run: |
|
||||
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
|
||||
{
|
||||
cat << 'GH_AW_PROMPT_767e1d181d9dae54_EOF'
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
<system>
|
||||
GH_AW_PROMPT_767e1d181d9dae54_EOF
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_767e1d181d9dae54_EOF'
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
<safe-output-tools>
|
||||
Tools: add_comment(max:2), create_pull_request, add_labels(max:3), missing_tool, missing_data, noop
|
||||
GH_AW_PROMPT_767e1d181d9dae54_EOF
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md"
|
||||
cat << 'GH_AW_PROMPT_767e1d181d9dae54_EOF'
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
</safe-output-tools>
|
||||
GH_AW_PROMPT_767e1d181d9dae54_EOF
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_767e1d181d9dae54_EOF'
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
<github-context>
|
||||
The following GitHub context information is available for this workflow:
|
||||
{{#if github.actor}}
|
||||
@@ -252,12 +252,12 @@ jobs:
|
||||
- **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches).
|
||||
</github-context>
|
||||
|
||||
GH_AW_PROMPT_767e1d181d9dae54_EOF
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_767e1d181d9dae54_EOF'
|
||||
cat << 'GH_AW_PROMPT_25355d452b4d239a_EOF'
|
||||
</system>
|
||||
{{#runtime-import .github/workflows/add-community-extension.md}}
|
||||
GH_AW_PROMPT_767e1d181d9dae54_EOF
|
||||
GH_AW_PROMPT_25355d452b4d239a_EOF
|
||||
} > "$GH_AW_PROMPT"
|
||||
- name: Interpolate variables and render templates
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -464,9 +464,9 @@ jobs:
|
||||
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
|
||||
mkdir -p /tmp/gh-aw/safeoutputs
|
||||
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_95f097d550e5bb4b_EOF'
|
||||
{"add_comment":{"max":2},"add_labels":{"allowed":["extension-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["extension-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[extension] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_95f097d550e5bb4b_EOF
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_a6227a6d6ade9e30_EOF'
|
||||
{"add_comment":{"max":2},"add_labels":{"allowed":["extension-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["extension-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[extension] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_a6227a6d6ade9e30_EOF
|
||||
- name: Generate Safe Outputs Tools
|
||||
env:
|
||||
GH_AW_TOOLS_META_JSON: |
|
||||
@@ -722,7 +722,7 @@ jobs:
|
||||
|
||||
mkdir -p /home/runner/.copilot
|
||||
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
|
||||
cat << GH_AW_MCP_CONFIG_9f16469ceb45c7f6_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
cat << GH_AW_MCP_CONFIG_6ce4129d4503180e_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
@@ -763,7 +763,7 @@ jobs:
|
||||
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
|
||||
}
|
||||
}
|
||||
GH_AW_MCP_CONFIG_9f16469ceb45c7f6_EOF
|
||||
GH_AW_MCP_CONFIG_6ce4129d4503180e_EOF
|
||||
- name: Mount MCP servers as CLIs
|
||||
id: mount-mcp-clis
|
||||
continue-on-error: true
|
||||
@@ -1079,7 +1079,7 @@ jobs:
|
||||
GH_AW_WORKFLOW_NAME: "Add Community Extension from Issue Submission"
|
||||
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
|
||||
GH_AW_NOOP_REPORT_AS_ISSUE: "false"
|
||||
GH_AW_NOOP_REPORT_AS_ISSUE: "true"
|
||||
with:
|
||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -1556,7 +1556,7 @@ jobs:
|
||||
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_API_URL: ${{ github.api_url }}
|
||||
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"extension-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"extension-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[extension] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
|
||||
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"extension-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"extension-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[extension] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
|
||||
GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
36
.github/workflows/add-community-preset.lock.yml
generated
vendored
36
.github/workflows/add-community-preset.lock.yml
generated
vendored
@@ -1,4 +1,4 @@
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f209d3fbcde6b25fd5099c7b1ea0d3dace8967b23d8049a92566c213ed9ccc5e","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"f6cbeb7bc3ee4de1c2b3963fbf21525d0add0425a6807a8335f8f9d93e01a44f","compiler_version":"v0.74.8","strict":true,"agent_id":"copilot"}
|
||||
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"efa55847f72aadb03490d955263ff911bf758700","version":"v0.74.8"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.49"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.49"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.4"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]}
|
||||
# ___ _ _
|
||||
# / _ \ | | (_)
|
||||
@@ -204,23 +204,23 @@ jobs:
|
||||
run: |
|
||||
bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
|
||||
{
|
||||
cat << 'GH_AW_PROMPT_c25ce620b285c8e3_EOF'
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
<system>
|
||||
GH_AW_PROMPT_c25ce620b285c8e3_EOF
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_c25ce620b285c8e3_EOF'
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
<safe-output-tools>
|
||||
Tools: add_comment(max:2), create_pull_request, add_labels(max:3), missing_tool, missing_data, noop
|
||||
GH_AW_PROMPT_c25ce620b285c8e3_EOF
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md"
|
||||
cat << 'GH_AW_PROMPT_c25ce620b285c8e3_EOF'
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
</safe-output-tools>
|
||||
GH_AW_PROMPT_c25ce620b285c8e3_EOF
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_c25ce620b285c8e3_EOF'
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
<github-context>
|
||||
The following GitHub context information is available for this workflow:
|
||||
{{#if github.actor}}
|
||||
@@ -252,12 +252,12 @@ jobs:
|
||||
- **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches).
|
||||
</github-context>
|
||||
|
||||
GH_AW_PROMPT_c25ce620b285c8e3_EOF
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
|
||||
cat << 'GH_AW_PROMPT_c25ce620b285c8e3_EOF'
|
||||
cat << 'GH_AW_PROMPT_26e9904027e0c5a2_EOF'
|
||||
</system>
|
||||
{{#runtime-import .github/workflows/add-community-preset.md}}
|
||||
GH_AW_PROMPT_c25ce620b285c8e3_EOF
|
||||
GH_AW_PROMPT_26e9904027e0c5a2_EOF
|
||||
} > "$GH_AW_PROMPT"
|
||||
- name: Interpolate variables and render templates
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
@@ -464,9 +464,9 @@ jobs:
|
||||
mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
|
||||
mkdir -p /tmp/gh-aw/safeoutputs
|
||||
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_50dbf4670371d6f7_EOF'
|
||||
{"add_comment":{"max":2},"add_labels":{"allowed":["preset-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["preset-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[preset] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_50dbf4670371d6f7_EOF
|
||||
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_36855fee66c4c038_EOF'
|
||||
{"add_comment":{"max":2},"add_labels":{"allowed":["preset-submission","validation-passed","validation-failed","needs-info"],"max":3},"create_pull_request":{"draft":true,"labels":["preset-submission","automated"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","CONTRIBUTING.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"blocked","title_prefix":"[preset] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
|
||||
GH_AW_SAFE_OUTPUTS_CONFIG_36855fee66c4c038_EOF
|
||||
- name: Generate Safe Outputs Tools
|
||||
env:
|
||||
GH_AW_TOOLS_META_JSON: |
|
||||
@@ -722,7 +722,7 @@ jobs:
|
||||
|
||||
mkdir -p /home/runner/.copilot
|
||||
GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
|
||||
cat << GH_AW_MCP_CONFIG_04e1e53849e8d680_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
cat << GH_AW_MCP_CONFIG_fdc26b942885c376_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
@@ -763,7 +763,7 @@ jobs:
|
||||
"payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
|
||||
}
|
||||
}
|
||||
GH_AW_MCP_CONFIG_04e1e53849e8d680_EOF
|
||||
GH_AW_MCP_CONFIG_fdc26b942885c376_EOF
|
||||
- name: Mount MCP servers as CLIs
|
||||
id: mount-mcp-clis
|
||||
continue-on-error: true
|
||||
@@ -1079,7 +1079,7 @@ jobs:
|
||||
GH_AW_WORKFLOW_NAME: "Add Community Preset from Issue Submission"
|
||||
GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
|
||||
GH_AW_NOOP_REPORT_AS_ISSUE: "false"
|
||||
GH_AW_NOOP_REPORT_AS_ISSUE: "true"
|
||||
with:
|
||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
@@ -1556,7 +1556,7 @@ jobs:
|
||||
GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
|
||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||
GITHUB_API_URL: ${{ github.api_url }}
|
||||
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"preset-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"preset-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[preset] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}"
|
||||
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"preset-submission\",\"validation-passed\",\"validation-failed\",\"needs-info\"],\"max\":3},\"create_pull_request\":{\"draft\":true,\"labels\":[\"preset-submission\",\"automated\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"CONTRIBUTING.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"blocked\",\"title_prefix\":\"[preset] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
|
||||
GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
|
||||
with:
|
||||
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
fetch-depth: 0 # Fetch all history for git info
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
|
||||
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
|
||||
|
||||
21
AGENTS.md
21
AGENTS.md
@@ -177,24 +177,7 @@ def _register_builtins() -> None:
|
||||
|
||||
Set `context_file` on the integration class. The base integration setup creates or updates the managed Spec Kit section in that file, and uninstall removes the managed section when appropriate.
|
||||
|
||||
The managed section is owned by the bundled `agent-context` extension (`extensions/agent-context/`). All configuration flows through the extension's own config file at `.specify/extensions/agent-context/agent-context-config.yml`:
|
||||
|
||||
```yaml
|
||||
# Path to the coding agent context file managed by this extension
|
||||
context_file: CLAUDE.md
|
||||
|
||||
# Delimiters for the managed Spec Kit section
|
||||
context_markers:
|
||||
start: "<!-- SPECKIT START -->"
|
||||
end: "<!-- SPECKIT END -->"
|
||||
```
|
||||
|
||||
- `context_file` is written automatically from the integration's class attribute when `specify init` or `specify integration use` is run.
|
||||
- `context_markers.{start,end}` defaults to `IntegrationBase.CONTEXT_MARKER_START` / `CONTEXT_MARKER_END`. Users who want custom markers edit `agent-context-config.yml` directly — both the Python layer (`upsert_context_section()` / `remove_context_section()`) and the bundled scripts (`extensions/agent-context/scripts/bash/update-agent-context.sh` and `.ps1`) read from this single source of truth.
|
||||
|
||||
Users can opt out entirely with `specify extension disable agent-context`; while disabled, Spec Kit skips context-file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
|
||||
|
||||
Only add custom setup logic when the agent needs non-standard behavior. Integrations no longer require per-agent thin wrapper scripts or shared context-update dispatcher scripts — the `agent-context` extension is fully generic.
|
||||
Only add custom setup logic when the agent needs non-standard behavior. Most integrations do not need wrapper scripts or separate context-update dispatch code.
|
||||
|
||||
### 5. Test it
|
||||
|
||||
@@ -426,7 +409,7 @@ When an issue exists, include its number immediately after the prefix — this i
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Using shorthand keys for CLI-based integrations**: For CLI-based integrations (`requires_cli: True`), the `key` must match the executable name (e.g., `"cursor-agent"` not `"cursor"`). `shutil.which(key)` is used for CLI tool checks — mismatches require special-case mappings. IDE-based integrations (`requires_cli: False`) are not subject to this constraint.
|
||||
2. **Forgetting context configuration**: The bundled `agent-context` extension reads from `.specify/extensions/agent-context/agent-context-config.yml`. New integrations only need to set `context_file` on the class — markers and dispatcher scripts are managed centrally.
|
||||
2. **Forgetting update scripts**: Both bash and PowerShell thin wrappers and the shared context-update scripts must be updated.
|
||||
3. **Incorrect `requires_cli` value**: Set to `True` only for agents that have a CLI tool; set to `False` for IDE-based agents.
|
||||
4. **Wrong argument format**: Use `$ARGUMENTS` for Markdown agents, `{{args}}` for TOML agents.
|
||||
5. **Skipping registration**: The import and `_register()` call in `_register_builtins()` must both be added.
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -2,20 +2,6 @@
|
||||
|
||||
<!-- insert new changelog below this comment -->
|
||||
|
||||
## [0.9.0] - 2026-06-01
|
||||
|
||||
### Changed
|
||||
|
||||
- chore: recompile workflow lock files (#2774)
|
||||
- Add Multi-Sites Spec Kit extension to community catalog (#2791)
|
||||
- Update Product Spec Extension to v0.8.3 (#2790)
|
||||
- Publish May 2026 Newsletter (#2787)
|
||||
- fix: move URL install confirmation prompt before spinner (#2783) (#2784)
|
||||
- Update Reqnroll BDD extension to v1.1.0 (#2775)
|
||||
- Extract agent context updates into bundled agent-context extension (#2546)
|
||||
- chore(deps): bump actions/setup-dotnet from 5.2.0 to 5.3.0 (#2755)
|
||||
- chore: release 0.8.18, begin 0.8.19.dev0 development (#2766)
|
||||
|
||||
## [0.8.18] - 2026-05-29
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -70,7 +70,6 @@ The following community-contributed extensions are available in [`catalog.commun
|
||||
| MemoryLint | Agent memory governance tool: Automatically audits and fixes boundary conflicts between AGENTS.md and the constitution. | `process` | Read+Write | [memorylint](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/memorylint) |
|
||||
| Microsoft 365 Integration | Fetch Teams messages, meeting transcripts, and SharePoint/OneDrive files as local Markdown for spec generation | `integration` | Read+Write | [spec-kit-m365](https://github.com/BenBtg/spec-kit-m365) |
|
||||
| Multi-Model Review | Cross-model Spec Kit handoffs for spec authoring, implementation routing, and review. | `process` | Read+Write | [multi-model-review](https://github.com/formin/multi-model-review) |
|
||||
| Multi-Sites Spec Kit | Multi-site aware specify command with per-site spec folders, auto-increment, and Drupal support | `process` | Read+Write | [spec-kit-multi-sites](https://github.com/teeyo/spec-kit-multi-sites) |
|
||||
| .NET Framework to Modern .NET Migration | Orchestrate end-to-end .NET Framework to modern .NET migration across 7 phases, with SDD lifecycle integration | `process` | Read+Write | [spec-kit-fx-to-net](https://github.com/RogerBestMsft/spec-kit-FxToNet) |
|
||||
| Onboard | Contextual onboarding and progressive growth for developers new to spec-kit projects. Explains specs, maps dependencies, validates understanding, and guides the next step | `process` | Read+Write | [spec-kit-onboard](https://github.com/dmux/spec-kit-onboard) |
|
||||
| Optimize | Audit and optimize AI governance for context efficiency — token budgets, rule health, interpretability, compression, coherence, and echo detection | `process` | Read+Write | [spec-kit-optimize](https://github.com/sakitA/spec-kit-optimize) |
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
# Coding Agent Context Extension
|
||||
|
||||
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 -->`).
|
||||
|
||||
## Why an extension?
|
||||
|
||||
Not every Spec Kit user wants Spec Kit to write into the coding agent's context file. Extracting this behavior into a dedicated extension lets users:
|
||||
|
||||
- **Opt out** entirely with `specify extension disable agent-context` — Spec Kit will then never create or modify the agent context file.
|
||||
- **Customize the markers** by editing `.specify/extensions/agent-context/agent-context-config.yml` — both the Python layer and the bundled scripts honor the same `context_markers` value.
|
||||
- **Refresh on demand** with `/speckit.agent-context.update`, or automatically through the hooks declared in `extension.yml` (`after_specify`, `after_plan`).
|
||||
|
||||
## Commands
|
||||
|
||||
| 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`:
|
||||
|
||||
```yaml
|
||||
# Path to the coding agent context file managed by this extension
|
||||
context_file: 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, written by `specify init` and `specify integration install`.
|
||||
- `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:
|
||||
|
||||
```bash
|
||||
pip install pyyaml
|
||||
# or target the specific interpreter Spec Kit uses:
|
||||
/path/to/speckit-python -m pip install pyyaml
|
||||
```
|
||||
|
||||
## Disable
|
||||
|
||||
```bash
|
||||
specify extension disable agent-context
|
||||
```
|
||||
|
||||
When disabled, Spec Kit skips context file creation, updates, and removal (the gates are inside `upsert_context_section()` and `remove_context_section()`).
|
||||
@@ -1,15 +0,0 @@
|
||||
# Coding Agent Context Extension Configuration
|
||||
# These values are populated automatically by `specify init` and
|
||||
# `specify integration use` / `specify integration install`.
|
||||
|
||||
# Path (relative to the project root) to the 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: ""
|
||||
|
||||
# Delimiters for the managed Spec Kit section.
|
||||
# Edit these to use custom markers.
|
||||
context_markers:
|
||||
start: "<!-- SPECKIT START -->"
|
||||
end: "<!-- SPECKIT END -->"
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
description: "Refresh the managed Spec Kit section in the coding agent context file"
|
||||
---
|
||||
|
||||
# Update Coding Agent Context
|
||||
|
||||
Refresh the managed Spec Kit section inside the active coding agent's context/instruction file (e.g. `CLAUDE.md`, `.github/copilot-instructions.md`, `AGENTS.md`).
|
||||
|
||||
## Behavior
|
||||
|
||||
The script reads the agent-context extension config at
|
||||
`.specify/extensions/agent-context/agent-context-config.yml` to discover:
|
||||
|
||||
- `context_file` — the path of the coding agent context file to manage.
|
||||
- `context_markers.start` / `.end` — the delimiters surrounding the managed section. Defaults to `<!-- SPECKIT START -->` and `<!-- SPECKIT END -->` when the field is missing.
|
||||
|
||||
It then creates, replaces, or appends the managed block so that the section points at the most recent plan path when one can be discovered (`specs/<feature>/plan.md`).
|
||||
|
||||
If `context_file` is empty or the file cannot be located, the command reports nothing to do and exits successfully.
|
||||
|
||||
## Execution
|
||||
|
||||
- **Bash**: `.specify/extensions/agent-context/scripts/bash/update-agent-context.sh [plan_path]`
|
||||
- **PowerShell**: `.specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1 [plan_path]`
|
||||
|
||||
When `plan_path` is omitted, the script auto-detects the most recently modified `specs/*/plan.md`.
|
||||
@@ -1,34 +0,0 @@
|
||||
schema_version: "1.0"
|
||||
|
||||
extension:
|
||||
id: agent-context
|
||||
name: "Coding Agent Context"
|
||||
version: "1.0.0"
|
||||
description: "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers"
|
||||
author: spec-kit-core
|
||||
repository: https://github.com/github/spec-kit
|
||||
license: MIT
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.2.0"
|
||||
|
||||
provides:
|
||||
commands:
|
||||
- name: speckit.agent-context.update
|
||||
file: commands/speckit.agent-context.update.md
|
||||
description: "Refresh the managed Spec Kit section in the coding agent context file"
|
||||
|
||||
hooks:
|
||||
after_specify:
|
||||
command: speckit.agent-context.update
|
||||
optional: true
|
||||
description: "Refresh agent context after specification"
|
||||
after_plan:
|
||||
command: speckit.agent-context.update
|
||||
optional: true
|
||||
description: "Refresh agent context after planning"
|
||||
|
||||
tags:
|
||||
- "agent"
|
||||
- "context"
|
||||
- "core"
|
||||
@@ -1,200 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# update-agent-context.sh
|
||||
#
|
||||
# Refresh the managed Spec Kit section in the coding agent's context file
|
||||
# (e.g. CLAUDE.md, .github/copilot-instructions.md, AGENTS.md).
|
||||
#
|
||||
# Reads `context_file` and `context_markers.{start,end}` from the
|
||||
# agent-context extension config:
|
||||
# .specify/extensions/agent-context/agent-context-config.yml
|
||||
#
|
||||
# Usage: update-agent-context.sh [plan_path]
|
||||
#
|
||||
# When `plan_path` is omitted, the script picks the most recently modified
|
||||
# `specs/*/plan.md` if any exist, otherwise emits the section without a
|
||||
# concrete plan path.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(pwd)"
|
||||
EXT_CONFIG="$PROJECT_ROOT/.specify/extensions/agent-context/agent-context-config.yml"
|
||||
DEFAULT_START="<!-- SPECKIT START -->"
|
||||
DEFAULT_END="<!-- SPECKIT END -->"
|
||||
|
||||
if [[ ! -f "$EXT_CONFIG" ]]; then
|
||||
echo "agent-context: $EXT_CONFIG not found; nothing to do." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Locate a suitable Python interpreter (python3, then python).
|
||||
_python=""
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
_python="python3"
|
||||
elif command -v python >/dev/null 2>&1 && python --version 2>&1 | grep -q "^Python 3"; then
|
||||
_python="python"
|
||||
fi
|
||||
|
||||
if [[ -z "$_python" ]]; then
|
||||
echo "agent-context: Python 3 not found on PATH; skipping update." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse extension config once; emit three newline-separated fields:
|
||||
# context_file, context_markers.start, context_markers.end
|
||||
if ! _raw_opts="$("$_python" - "$EXT_CONFIG" <<'PY'
|
||||
import sys
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print(
|
||||
"agent-context: PyYAML is required to parse extension config but is not available "
|
||||
"in the current Python environment.\n"
|
||||
" To resolve: pip install pyyaml (or install it into the environment used by python3).\n"
|
||||
" Context file will not be updated until PyYAML is importable.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
try:
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"agent-context: unable to parse {sys.argv[1]} ({exc}); cannot update context.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
def get_str(obj, *keys):
|
||||
node = obj
|
||||
for k in keys:
|
||||
if isinstance(node, dict) and k in node:
|
||||
node = node[k]
|
||||
else:
|
||||
return ""
|
||||
return node if isinstance(node, str) else ""
|
||||
print(get_str(data, "context_file"))
|
||||
print(get_str(data, "context_markers", "start"))
|
||||
print(get_str(data, "context_markers", "end"))
|
||||
PY
|
||||
)"; then
|
||||
echo "agent-context: skipping update (see above for details)." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
_opts_lines=()
|
||||
while IFS= read -r _line || [[ -n "$_line" ]]; do
|
||||
_opts_lines+=("$_line")
|
||||
done < <(printf '%s\n' "$_raw_opts")
|
||||
if (( ${#_opts_lines[@]} < 3 )); then
|
||||
echo "agent-context: malformed config parser output; expected 3 lines (context_file, marker_start, marker_end), got ${#_opts_lines[@]}; skipping update." >&2
|
||||
exit 0
|
||||
fi
|
||||
CONTEXT_FILE="${_opts_lines[0]}"
|
||||
MARKER_START="${_opts_lines[1]}"
|
||||
MARKER_END="${_opts_lines[2]}"
|
||||
|
||||
if [[ -z "$CONTEXT_FILE" ]]; then
|
||||
echo "agent-context: context_file not set in extension config; nothing to do." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Reject absolute paths, backslash separators, and '..' path segments in context_file
|
||||
if [[ "$CONTEXT_FILE" == /* ]] || [[ "$CONTEXT_FILE" =~ ^[A-Za-z]: ]]; then
|
||||
echo "agent-context: context_file must be a project-relative path; got '$CONTEXT_FILE'." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$CONTEXT_FILE" == *\\* ]]; then
|
||||
echo "agent-context: context_file must not contain backslash separators; got '$CONTEXT_FILE'." >&2
|
||||
exit 1
|
||||
fi
|
||||
IFS='/' read -ra _cf_parts <<< "$CONTEXT_FILE"
|
||||
for _seg in "${_cf_parts[@]}"; do
|
||||
if [[ "$_seg" == ".." ]]; then
|
||||
echo "agent-context: context_file must not contain '..' path segments; got '$CONTEXT_FILE'." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
unset _cf_parts _seg
|
||||
|
||||
[[ -z "$MARKER_START" ]] && MARKER_START="$DEFAULT_START"
|
||||
[[ -z "$MARKER_END" ]] && MARKER_END="$DEFAULT_END"
|
||||
|
||||
PLAN_PATH="${1:-}"
|
||||
if [[ -z "$PLAN_PATH" ]]; then
|
||||
# Pick the most recently modified plan.md one level deep (specs/<feature>/plan.md).
|
||||
# Use find + sort by modification time to avoid ls/head fragility with
|
||||
# spaces in paths or SIGPIPE from pipefail.
|
||||
_plan_abs="$("$_python" - "$PROJECT_ROOT" <<'PY'
|
||||
import sys, os
|
||||
from pathlib import Path
|
||||
specs = Path(sys.argv[1]) / "specs"
|
||||
plans = sorted(
|
||||
specs.glob("*/plan.md"),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
print(plans[0] if plans else "")
|
||||
PY
|
||||
)"
|
||||
if [[ -n "$_plan_abs" ]]; then
|
||||
PLAN_PATH="${_plan_abs#"$PROJECT_ROOT/"}"
|
||||
fi
|
||||
fi
|
||||
|
||||
CTX_PATH="$PROJECT_ROOT/$CONTEXT_FILE"
|
||||
mkdir -p "$(dirname "$CTX_PATH")"
|
||||
|
||||
# Build the managed section
|
||||
TMP_SECTION="$(mktemp)"
|
||||
trap 'rm -f "$TMP_SECTION"' EXIT
|
||||
{
|
||||
echo "$MARKER_START"
|
||||
echo "For additional context about technologies to be used, project structure,"
|
||||
echo "shell commands, and other important information, read the current plan"
|
||||
if [[ -n "$PLAN_PATH" ]]; then
|
||||
echo "at $PLAN_PATH"
|
||||
fi
|
||||
echo "$MARKER_END"
|
||||
} > "$TMP_SECTION"
|
||||
|
||||
"$_python" - "$CTX_PATH" "$MARKER_START" "$MARKER_END" "$TMP_SECTION" <<'PY'
|
||||
import sys, os
|
||||
ctx_path, start, end, section_path = sys.argv[1:5]
|
||||
with open(section_path, "r", encoding="utf-8") as fh:
|
||||
section = fh.read().rstrip("\n") + "\n"
|
||||
|
||||
if os.path.exists(ctx_path):
|
||||
with open(ctx_path, "r", encoding="utf-8-sig") as fh:
|
||||
content = fh.read()
|
||||
s = content.find(start)
|
||||
e = content.find(end, s if s != -1 else 0)
|
||||
if s != -1 and e != -1 and e > s:
|
||||
end_of_marker = e + len(end)
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
||||
end_of_marker += 1
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
||||
end_of_marker += 1
|
||||
new_content = content[:s] + section + content[end_of_marker:]
|
||||
elif s != -1:
|
||||
new_content = content[:s] + section
|
||||
elif e != -1:
|
||||
end_of_marker = e + len(end)
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
||||
end_of_marker += 1
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
||||
end_of_marker += 1
|
||||
new_content = section + content[end_of_marker:]
|
||||
else:
|
||||
if content and not content.endswith("\n"):
|
||||
content += "\n"
|
||||
new_content = (content + "\n" + section) if content else section
|
||||
else:
|
||||
new_content = section
|
||||
|
||||
new_content = new_content.replace("\r\n", "\n").replace("\r", "\n")
|
||||
with open(ctx_path, "wb") as fh:
|
||||
fh.write(new_content.encode("utf-8"))
|
||||
PY
|
||||
|
||||
echo "agent-context: updated $CONTEXT_FILE"
|
||||
@@ -1,237 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# update-agent-context.ps1
|
||||
#
|
||||
# Refresh the managed Spec Kit section in the coding agent's context file
|
||||
# (e.g. CLAUDE.md, .github/copilot-instructions.md, AGENTS.md).
|
||||
#
|
||||
# Reads `context_file` and `context_markers.{start,end}` from the
|
||||
# agent-context extension config:
|
||||
# .specify/extensions/agent-context/agent-context-config.yml
|
||||
#
|
||||
# Usage: update-agent-context.ps1 [plan_path]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position = 0)]
|
||||
[string]$PlanPath
|
||||
)
|
||||
|
||||
function Get-ConfigValue {
|
||||
param(
|
||||
[AllowNull()][object]$Object,
|
||||
[Parameter(Mandatory = $true)][string]$Key
|
||||
)
|
||||
|
||||
if ($null -eq $Object) {
|
||||
return $null
|
||||
}
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
return $Object[$Key]
|
||||
}
|
||||
$prop = $Object.PSObject.Properties[$Key]
|
||||
if ($prop) {
|
||||
return $prop.Value
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Test-ConfigObject {
|
||||
param(
|
||||
[AllowNull()][object]$Object
|
||||
)
|
||||
|
||||
if ($null -eq $Object) {
|
||||
return $false
|
||||
}
|
||||
if ($Object -is [System.Collections.IDictionary]) {
|
||||
return $true
|
||||
}
|
||||
if ($Object -is [System.Management.Automation.PSCustomObject]) {
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$DefaultStart = '<!-- SPECKIT START -->'
|
||||
$DefaultEnd = '<!-- SPECKIT END -->'
|
||||
$ProjectRoot = (Get-Location).Path
|
||||
$ExtConfig = Join-Path $ProjectRoot '.specify/extensions/agent-context/agent-context-config.yml'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ExtConfig)) {
|
||||
Write-Warning "agent-context: $ExtConfig not found; nothing to do."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$Options = $null
|
||||
if (Get-Command ConvertFrom-Yaml -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
$Options = Get-Content -LiteralPath $ExtConfig -Raw | ConvertFrom-Yaml -ErrorAction Stop
|
||||
} catch {
|
||||
# fall through to Python fallback
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -eq $Options) {
|
||||
# ConvertFrom-Yaml unavailable or failed; fall back to Python+PyYAML.
|
||||
$pythonCmd = $null
|
||||
foreach ($candidate in @('python3', 'python')) {
|
||||
if (Get-Command $candidate -ErrorAction SilentlyContinue) {
|
||||
# Verify it is Python 3
|
||||
$verOut = & $candidate --version 2>&1
|
||||
if ($verOut -match 'Python 3') {
|
||||
$pythonCmd = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($pythonCmd) {
|
||||
try {
|
||||
$jsonOut = & $pythonCmd -c @'
|
||||
import json
|
||||
import sys
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print(
|
||||
"agent-context: PyYAML is required to parse extension config; cannot update context.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
try:
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"agent-context: unable to parse {sys.argv[1]} ({exc}); cannot update context.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
if not isinstance(data, dict):
|
||||
data = {}
|
||||
|
||||
print(json.dumps(data))
|
||||
'@ $ExtConfig
|
||||
if ($LASTEXITCODE -eq 0 -and $jsonOut) {
|
||||
$Options = $jsonOut | ConvertFrom-Json -ErrorAction Stop
|
||||
}
|
||||
} catch {
|
||||
$Options = $null
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Options) {
|
||||
Write-Warning "agent-context: unable to parse $ExtConfig; skipping update."
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-ConfigObject -Object $Options)) {
|
||||
Write-Warning "agent-context: $ExtConfig must contain a YAML mapping; skipping update."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$ContextFile = Get-ConfigValue -Object $Options -Key 'context_file'
|
||||
if (-not $ContextFile) {
|
||||
Write-Warning 'agent-context: context_file not set in extension config; nothing to do.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Reject absolute paths and '..' path segments in context_file
|
||||
if ([System.IO.Path]::IsPathRooted($ContextFile)) {
|
||||
Write-Warning "agent-context: context_file must be a project-relative path; got '$ContextFile'."
|
||||
exit 1
|
||||
}
|
||||
$cfSegments = $ContextFile -split '[/\\]'
|
||||
if ($cfSegments -contains '..') {
|
||||
Write-Warning "agent-context: context_file must not contain '..' path segments; got '$ContextFile'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$MarkerStart = $DefaultStart
|
||||
$MarkerEnd = $DefaultEnd
|
||||
$cm = Get-ConfigValue -Object $Options -Key 'context_markers'
|
||||
if ($cm) {
|
||||
$cmStart = Get-ConfigValue -Object $cm -Key 'start'
|
||||
if ($cmStart -is [string] -and $cmStart) {
|
||||
$MarkerStart = $cmStart
|
||||
}
|
||||
$cmEnd = Get-ConfigValue -Object $cm -Key 'end'
|
||||
if ($cmEnd -is [string] -and $cmEnd) {
|
||||
$MarkerEnd = $cmEnd
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $PlanPath) {
|
||||
# Discover plan.md exactly one level deep (specs/<feature>/plan.md),
|
||||
# matching the bash glob specs/*/plan.md. Wrap in try/catch so access errors under
|
||||
# $ErrorActionPreference = 'Stop' don't abort the script.
|
||||
try {
|
||||
$specsDir = Join-Path $ProjectRoot 'specs'
|
||||
$candidate = Get-ChildItem -Path $specsDir -Directory -ErrorAction SilentlyContinue |
|
||||
ForEach-Object { Get-Item -LiteralPath (Join-Path $_.FullName 'plan.md') -ErrorAction SilentlyContinue } |
|
||||
Where-Object { $_ } |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if ($candidate) {
|
||||
$PlanPath = [System.IO.Path]::GetRelativePath($ProjectRoot, $candidate.FullName).Replace('\','/')
|
||||
}
|
||||
} catch {
|
||||
# Non-fatal: continue without a plan path.
|
||||
}
|
||||
}
|
||||
|
||||
$CtxPath = Join-Path $ProjectRoot $ContextFile
|
||||
$CtxDir = Split-Path -Parent $CtxPath
|
||||
if ($CtxDir -and -not (Test-Path -LiteralPath $CtxDir)) {
|
||||
New-Item -ItemType Directory -Path $CtxDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$lines = @($MarkerStart,
|
||||
'For additional context about technologies to be used, project structure,',
|
||||
'shell commands, and other important information, read the current plan')
|
||||
if ($PlanPath) {
|
||||
$lines += "at $PlanPath"
|
||||
}
|
||||
$lines += $MarkerEnd
|
||||
$Section = ($lines -join "`n") + "`n"
|
||||
|
||||
if (Test-Path -LiteralPath $CtxPath) {
|
||||
$rawBytes = [System.IO.File]::ReadAllBytes($CtxPath)
|
||||
# Strip UTF-8 BOM if present
|
||||
if ($rawBytes.Length -ge 3 -and $rawBytes[0] -eq 0xEF -and $rawBytes[1] -eq 0xBB -and $rawBytes[2] -eq 0xBF) {
|
||||
$content = [System.Text.Encoding]::UTF8.GetString($rawBytes, 3, $rawBytes.Length - 3)
|
||||
} else {
|
||||
$content = [System.Text.Encoding]::UTF8.GetString($rawBytes)
|
||||
}
|
||||
|
||||
$s = $content.IndexOf($MarkerStart)
|
||||
$e = if ($s -ge 0) { $content.IndexOf($MarkerEnd, $s) } else { $content.IndexOf($MarkerEnd) }
|
||||
|
||||
if ($s -ge 0 -and $e -ge 0 -and $e -gt $s) {
|
||||
$endOfMarker = $e + $MarkerEnd.Length
|
||||
if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`r") { $endOfMarker++ }
|
||||
if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`n") { $endOfMarker++ }
|
||||
$newContent = $content.Substring(0, $s) + $Section + $content.Substring($endOfMarker)
|
||||
} elseif ($s -ge 0) {
|
||||
$newContent = $content.Substring(0, $s) + $Section
|
||||
} elseif ($e -ge 0) {
|
||||
$endOfMarker = $e + $MarkerEnd.Length
|
||||
if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`r") { $endOfMarker++ }
|
||||
if ($endOfMarker -lt $content.Length -and $content[$endOfMarker] -eq "`n") { $endOfMarker++ }
|
||||
$newContent = $Section + $content.Substring($endOfMarker)
|
||||
} else {
|
||||
if ($content -and -not $content.EndsWith("`n")) { $content += "`n" }
|
||||
if ($content) { $newContent = $content + "`n" + $Section } else { $newContent = $Section }
|
||||
}
|
||||
} else {
|
||||
$newContent = $Section
|
||||
}
|
||||
|
||||
$newContent = $newContent.Replace("`r`n", "`n").Replace("`r", "`n")
|
||||
[System.IO.File]::WriteAllText($CtxPath, $newContent, (New-Object System.Text.UTF8Encoding($false)))
|
||||
|
||||
Write-Host "agent-context: updated $ContextFile"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-06-01T00:00:00Z",
|
||||
"updated_at": "2026-05-28T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
|
||||
"extensions": {
|
||||
"aide": {
|
||||
@@ -1724,37 +1724,6 @@
|
||||
"created_at": "2026-05-04T02:51:52Z",
|
||||
"updated_at": "2026-05-04T02:51:52Z"
|
||||
},
|
||||
"multi-sites": {
|
||||
"name": "Multi-Sites Spec Kit",
|
||||
"id": "multi-sites",
|
||||
"description": "Multi-site aware specify command with per-site spec folders, auto-increment, and Drupal support",
|
||||
"author": "teeyo",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/teeyo/spec-kit-multi-sites/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/teeyo/spec-kit-multi-sites",
|
||||
"homepage": "https://github.com/teeyo/spec-kit-multi-sites",
|
||||
"documentation": "https://github.com/teeyo/spec-kit-multi-sites/blob/main/README.md",
|
||||
"changelog": "https://github.com/teeyo/spec-kit-multi-sites/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.1.0"
|
||||
},
|
||||
"provides": {
|
||||
"commands": 1,
|
||||
"hooks": 0
|
||||
},
|
||||
"tags": [
|
||||
"multi-site",
|
||||
"drupal",
|
||||
"workflow",
|
||||
"process"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-06-01T00:00:00Z",
|
||||
"updated_at": "2026-06-01T00:00:00Z"
|
||||
},
|
||||
"onboard": {
|
||||
"name": "Onboard",
|
||||
"id": "onboard",
|
||||
@@ -1981,12 +1950,12 @@
|
||||
"name": "Product Spec Extension",
|
||||
"id": "product",
|
||||
"description": "Generates PRFAQ, Lean PRD, stakeholder summaries, and technical designs from engineering specs.",
|
||||
"author": "d0whc3r",
|
||||
"version": "0.8.3",
|
||||
"download_url": "https://github.com/d0whc3r/spec-kit-product/releases/download/v0.8.3/product-0.8.3.zip",
|
||||
"author": "spec-kit-product contributors",
|
||||
"version": "0.1.3",
|
||||
"download_url": "https://github.com/d0whc3r/spec-kit-product/releases/download/v0.1.3/product-0.1.3.zip",
|
||||
"repository": "https://github.com/d0whc3r/spec-kit-product",
|
||||
"homepage": "https://d0whc3r.github.io/spec-kit-product/",
|
||||
"documentation": "https://github.com/d0whc3r/spec-kit-product/wiki",
|
||||
"homepage": "https://github.com/d0whc3r/spec-kit-product",
|
||||
"documentation": "https://github.com/d0whc3r/spec-kit-product/blob/main/README.md",
|
||||
"changelog": "https://github.com/d0whc3r/spec-kit-product/blob/main/CHANGELOG.md",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
@@ -1994,30 +1963,20 @@
|
||||
},
|
||||
"provides": {
|
||||
"commands": 4,
|
||||
"hooks": 3
|
||||
"hooks": 6
|
||||
},
|
||||
"tags": [
|
||||
"design",
|
||||
"documentation",
|
||||
"jtbd",
|
||||
"lean-prd",
|
||||
"planning",
|
||||
"prd",
|
||||
"prfaq",
|
||||
"product",
|
||||
"product-management",
|
||||
"requirements",
|
||||
"spec",
|
||||
"spec-kit",
|
||||
"spec-kit-extension",
|
||||
"stakeholder",
|
||||
"technical-design"
|
||||
"prd",
|
||||
"design",
|
||||
"documentation"
|
||||
],
|
||||
"verified": false,
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-26T00:00:00Z",
|
||||
"updated_at": "2026-06-01T00:00:00Z"
|
||||
"updated_at": "2026-05-26T00:00:00Z"
|
||||
},
|
||||
"product-forge": {
|
||||
"name": "Product Forge",
|
||||
@@ -2259,8 +2218,8 @@
|
||||
"id": "reqnroll-bdd",
|
||||
"description": "Adds Reqnroll BDD planning, Gherkin generation, traceability, safe task injection, handoff, and verification to Spec Kit.",
|
||||
"author": "LoogaCY Studio",
|
||||
"version": "1.1.0",
|
||||
"download_url": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd/archive/refs/tags/v1.1.0.zip",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd/archive/refs/tags/v1.0.0.zip",
|
||||
"repository": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd",
|
||||
"homepage": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd",
|
||||
"documentation": "https://github.com/LoogacyStudio/spec-kit-reqnroll-bdd#readme",
|
||||
@@ -2290,7 +2249,7 @@
|
||||
"downloads": 0,
|
||||
"stars": 0,
|
||||
"created_at": "2026-05-13T00:00:00Z",
|
||||
"updated_at": "2026-05-30T00:00:00Z"
|
||||
"updated_at": "2026-05-13T00:00:00Z"
|
||||
},
|
||||
"retro": {
|
||||
"name": "Retro Extension",
|
||||
|
||||
@@ -3,20 +3,6 @@
|
||||
"updated_at": "2026-04-10T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.json",
|
||||
"extensions": {
|
||||
"agent-context": {
|
||||
"name": "Coding Agent Context",
|
||||
"id": "agent-context",
|
||||
"version": "1.0.0",
|
||||
"description": "Manages coding agent context/instruction files (e.g., CLAUDE.md, copilot-instructions.md) with project-specific plan references and configurable markers",
|
||||
"author": "spec-kit-core",
|
||||
"repository": "https://github.com/github/spec-kit",
|
||||
"bundled": true,
|
||||
"tags": [
|
||||
"agent",
|
||||
"context",
|
||||
"core"
|
||||
]
|
||||
},
|
||||
"git": {
|
||||
"name": "Git Branching Workflow",
|
||||
"id": "git",
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# Spec Kit - May 2026 Newsletter
|
||||
|
||||
This edition covers Spec Kit activity in May 2026 — a month defined by three milestone 100s: **100,000+ stars**, **100+ community extensions**, and recognition as a **top-100 GitHub project**. Fourteen releases shipped (v0.8.4 through v0.8.17), delivering multi-agent install support, constitution governance enforcement, and continued architecture cleanup. The Open Source Friday livestream, a wave of multilingual coverage, and analyst recognition from The Futurum Group marked the project's transition from fast-moving experiment to established ecosystem. A summary is in the table below, followed by details.
|
||||
|
||||
| **Spec Kit Core (May 2026)** | **Community & Content** | **SDD Ecosystem & Next** |
|
||||
| --- | --- | --- |
|
||||
| Fourteen releases shipped with key features: multi-install for concurrent agent integrations, constitution governance in implement, authentication provider registry, Hermes and Lingma agents, and a `__init__.py` decomposition series. The repo grew from ~92k to **106,951 stars**, crossing **100K** on May 21. [\[github.com\]](https://github.com/github/spec-kit/releases) | The community extension catalog crossed **100 entries** (now 105). Open Source Friday livestream drove a press wave: Visual Studio Magazine, DevOps.com, MarkTechPost, HackerNoon, and 25+ more articles — now tracked across multiple languages following an expanded discovery methodology. **217 contributors** now listed. | MarkTechPost called Spec Kit "the most community-adopted open-source option" for SDD. The Futurum Group's Mitch Ashley framed specs as "the unit of governance across agents and contributors." Truong Phung published a 61-min production playbook referencing Spec Kit. Competitors grew but differentiate on orchestration; Spec Kit leads in portability and community. |
|
||||
|
||||
***
|
||||
|
||||
> **A Month of 100s.** May 2026 was defined by three milestones that all share the same number. The community extension catalog crossed **100 entries** during the week of May 21, making Spec Kit a genuine platform with more capabilities in its ecosystem than in its core. The repository crossed **100,000 GitHub stars** on the same week. And with 107K stars at month's end, Spec Kit now ranks among the **top 100 most-starred projects on all of GitHub**. None of this would have happened without the community — the contributors, extension authors, preset builders, article writers, and practitioners who turned a spec-driven development experiment into an ecosystem. Thank you.
|
||||
|
||||
## Spec Kit Project Updates
|
||||
|
||||
### Releases Overview
|
||||
|
||||
**v0.8.4–v0.8.7** (May 1–7) opened the month with four patch releases delivering the most-requested feature of the year: **multi-install support for concurrent AI agent integrations** (#2389), enabling multiple agents in a single project. This closed five long-standing issues dating back 228 days. The releases also added **constitution governance in `/speckit.implement`** (#2460), ensuring the implement phase now loads `constitution.md` to enforce governance during code generation. An **authentication provider registry** (#2393) added config-driven multi-platform auth. The **Lingma agent** joined the integration roster. Security hardening included pinning all remaining GitHub Actions to immutable SHAs (#2441) and URL scheme validation to prevent SSRF-style bugs (#2449). Seven new community extensions and six new governance-themed presets landed. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
**v0.8.8–v0.8.10** (May 8–14) shipped three releases focused on stability. **Version feature reporting** (#2548) improved upgrade visibility. Bug fixes addressed the Kiro CLI `$ARGUMENTS` placeholder (#1926, open 52 days), markdownlint-safe template metadata (#1343, open 147 days), and preset skill description precedence. The `__init__.py` decomposition series began with PRs 1–2/8, extracting `_console.py`, `_assets.py`, and `_utils.py`. Seven new extensions joined (Architecture Workflow, Agent Governance, BrownKit, Schedule, Reqnroll BDD, MDE, Changelog) along with two new presets (MDE, game-narrative-writing). The docs site received a major overhaul: the landing page was revamped with a four-pillar card layout, the install section was streamlined, and the community extensions table moved to the docs site. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
**v0.8.11–v0.8.13** (May 15–21) delivered three releases as the repo **crossed 100K stars**. **Agentic catalog submissions** (#2655) added AI-assisted workflows for community catalog contributions. A **high-assurance spec workflow** was documented (#2518). The while/do-while loop stale output bug (#2592) was caught and fixed same-day. **Integration auto mode** (#2421) now follows the project's initialized AI instead of hardcoding Copilot. The PowerShell UTF-8 BOM issue (#2280) was resolved. Four new extensions joined (Team Assign, Interactive HTML Preview, Time Machine, Superpowers Implementation Bridge), bringing the catalog to **103 entries** — crossing the 100 mark. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
**v0.8.14–v0.8.17** (May 22–28) closed the month with four releases. The **Hermes Agent** joined as a new integration target (#2651). Workflows gained a **`{{ context.run_id }}` template variable** (#2664). A new `SPECKIT_INTEGRATION_<KEY>_EXTRA_ARGS` environment variable (#2596) lets users pass extra flags to agent subprocesses. **Extension installs from URLs now prompt for confirmation** (#2745), a security improvement for URL-based installs. The spec quality checklist is now **re-validated after clarify updates the spec** (#2715). Token Budget, Product Spec, and Workflow Preset extensions joined the catalog, bringing it to **105 entries**. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### Architecture & Refactoring
|
||||
|
||||
The most significant internal effort in May was the **`__init__.py` decomposition series**, progressing through PRs 1–4 of 8. This systematic extraction moved `_console.py`, `_assets.py`, `_utils.py`, `_version.py`, and the `commands/` package out of the monolithic init module, improving maintainability and contributor onboarding. The **ExtensionCatalog was migrated to the shared catalog stack base** (#2437), reducing duplicated catalog handling across extension, preset, and integration catalogs. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### Bug Fixes and Security
|
||||
|
||||
Fourteen releases produced a strong cadence of fixes. Long-standing issues resolved include the Kiro CLI `$ARGUMENTS` placeholder (52 days), markdownlint template metadata line breaks (147 days), and the `--ai` flag for adding agent commands (136 days). The PowerShell UTF-8 BOM issue was fixed, preset skill rendering now correctly resolves `__SPECKIT_COMMAND_*__` refs (#2717), and a Windows gate-step crash was addressed (#2635).
|
||||
|
||||
Security improvements included **URL-based extension install confirmation** (#2745), **pinning GitHub Actions to immutable SHAs** (#2441), **URL scheme validation** (#2449), and restricting community submission workflows to labeled events only (#2741). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
### The Extension & Preset Ecosystem
|
||||
|
||||
The community extension catalog grew from 92 to **105 entries** during May, crossing the **100 mark** on May 21. Thirteen new extensions were added over the month. Community presets grew from 18 to **21 entries**, with three new presets added.
|
||||
|
||||
Notable new extensions by category:
|
||||
|
||||
- **Architecture & governance**: Architecture Workflow (bigsmartben), Agent Governance (bigben), Architecture Guard (DyanGalih), BrownKit (Maksim Shautsou)
|
||||
- **Cost & token management**: Cost Tracker (Quratulain-bilal), Token Analyzer (Chris Roberts), Token Budget (Tine Kondo)
|
||||
- **Agent orchestration**: Agent Orchestrator (pragya247), Multi-Model Review (formin)
|
||||
- **Project management**: Team Assign (tarunkumarbhati), Changelog (Quratulain-bilal)
|
||||
- **Cloud & enterprise**: Spec2Cloud for Azure (Azure Samples), .NET Framework to Modern .NET Migration (RogerBestMsft)
|
||||
- **API & lifecycle**: API Evolve (Quratulain-bilal), Product Spec (spec-kit-product contributors)
|
||||
- **Quality**: Schedule with CP-SAT solver (Julio César Franco Ardila), Reqnroll BDD (LoogaCY Studio), MDE (AI-MDE)
|
||||
- **Spec exploration**: Interactive HTML Preview (bigsmartben), Time Machine (te3yo)
|
||||
- **Cross-tool bridges**: Superpowers Implementation Bridge (lihan3238)
|
||||
|
||||
New governance-themed presets dominated: a11y-governance, architecture-governance, security-governance, cross-platform-governance, agent-parity-governance, and Spec2Cloud preset. Creative presets included game-narrative-writing and MDE.
|
||||
|
||||
The extension ecosystem also showed maturation through active maintenance. **Architecture Guard** progressed through four releases (v1.6.7 → v1.8.9), adding documentation quality improvements and governance features. **Memory MD** shipped multiple updates (v0.6.9 → v0.8.0), adding a `speckit.memory-md.log-finding` command. **Security Review** reached v1.4.5 with a new `speckit.security-review.log-finding` command. **Superpowers Implementation Bridge** evolved rapidly (v0.5.0 → v0.7.0). **Squad Bridge** updated to v1.3.0, **Fiction Book Writing** to v1.8.1, **Security Governance** to v0.4.0, and **MemoryLint** to v1.4.0. [\[github.com\]](https://github.github.io/spec-kit/community/extensions.html)
|
||||
|
||||
### Documentation & Docs Site
|
||||
|
||||
The docs site received its most significant update since launch. The **landing page was revamped** with a four-pillar card layout (#2531). The **install section was streamlined** (#2561). The **community extensions table** was moved from the README to the docs site (#2560), reducing README length while improving discoverability. **Community sections in the README** were consolidated (#2736). The **uv installation guide** was added with inline callouts (#2465). Landing page stats and branch naming conventions were updated (#2727). [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
|
||||
## Community & Content
|
||||
|
||||
### The Open Source Friday Livestream
|
||||
|
||||
On **May 8**, the **GitHub Open Source Friday livestream** featured Spec Kit, hosted by Andrea Griffiths with lead maintainer Manfred Riem. The livestream demonstrated a full SDD workflow building a time-zone-aware command-line utility with GitHub Copilot in VS Code. Riem described AI agents as "a very capable intern and a very quick intern but it's still an intern nonetheless." He emphasized that "the spec is always the source of truth" and highlighted the community ecosystem, noting the project was "nearing the 100 mark" for extensions. The livestream drove significant press attention in the following days. [\[youtube.com\]](https://www.youtube.com/watch?v=2IArMAhkJcE)
|
||||
|
||||
### Press and Industry Coverage
|
||||
|
||||
May produced the broadest press coverage to date, with publications from the mainstream developer media covering Spec Kit for the first time.
|
||||
|
||||
**Visual Studio Magazine** (David Ramel, May 12) published *"GitHub Spec Kit Takes Off as Antidote to Piecemeal 'Vibe Coding'"*, reporting on the Open Source Friday livestream and the growing ecosystem. The article noted Spec Kit's story is "no longer just that GitHub open sourced a spec-driven development toolkit last fall" but that "the toolkit is becoming a fast-moving ecosystem for teams trying to make AI-assisted development more structured, repeatable and traceable." [\[visualstudiomagazine.com\]](https://visualstudiomagazine.com/articles/2026/05/12/github-spec-kit-takes-off-as-antidote-to-piecemeal-vibe-coding.aspx)
|
||||
|
||||
**DevOps.com** (Tom Smith, May 11) published *"GitHub's Spec Kit Puts the Spec Back in Software Development"*, featuring analyst commentary from The Futurum Group (see The Analyst View below). [\[devops.com\]](https://devops.com/githubs-spec-kit-puts-the-spec-back-in-software-development/)
|
||||
|
||||
**MarkTechPost** (Asif Razzaq, May 8) published two articles: a comprehensive step-by-step tutorial calling Spec Kit an open-source toolkit with "90k+ stars" and "one of the faster-growing developer tooling repositories," and a 9-tool SDD comparison calling Spec Kit **"the most community-adopted open-source option"** and "the default starting point for teams new to SDD." [\[marktechpost.com\]](https://www.marktechpost.com/2026/05/08/meet-github-spec-kit-an-open-source-toolkit-for-spec-driven-development-with-ai-coding-agents/)
|
||||
|
||||
**HackerNoon** (Andrey Kucherenko, May 6) published *"The Spec-First Development Showdown"*, a hands-on comparison of Spec Kit, OpenSpec, BMAD, and Gangsta Agents. [\[hackernoon.com\]](https://hackernoon.com/the-spec-first-development-showdown-spec-kit-openspec-bmad-and-gangsta-agents-compared)
|
||||
|
||||
### Developer Articles and Blog Posts
|
||||
|
||||
May produced a wave of independent coverage — well beyond any previous month. Starting this month, article discovery was expanded beyond English-centric search engines to include language-appropriate engines for 25+ languages, so the broader coverage partly reflects wider discovery rather than a sudden spike.
|
||||
|
||||
Notable non-English coverage:
|
||||
|
||||
- **Japanese**: テックオーシャン published a detailed experience report on *"Claude Code × Spec Kit"* on note.com, praising task decomposition accuracy while noting spec sync requires manual workarounds. [\[note.com\]](https://note.com/techocean_corp/n/nd2bd63106c16)
|
||||
- **Portuguese**: Jady Sobjak de Mello Godoi published *"GitHub Spec Kit: Revolucionando o Desenvolvimento com SDD"* on DEV Community. [\[dev.to\]](https://dev.to/jadysmgodoi/github-speckit-revolucionando-o-desenvolvimento-com-sdd-l66)
|
||||
- **Italian**: Cosmonet published a comprehensive guide, *"GitHub Spec Kit: la guida completa allo Spec-Driven Development."* [\[cosmonet.info\]](https://www.cosmonet.info/github-spec-kit-guida-spec-driven-development/)
|
||||
- **French**: InnoSpira covered Spec Kit's rapid growth past 100K stars. [\[innospira.fr\]](https://www.innospira.fr/index.php/2026/05/12/github-spec-kit-place-au-developpement-pilote-par-la-spec/)
|
||||
- **Spanish**: Q2B Studio published an overview for Spanish-speaking developers. [\[q2bstudio.com\]](https://www.q2bstudio.com/nuestro-blog/1727819/github-spec-kit-desarrollo-especificaciones-ia)
|
||||
|
||||
Notable English-language articles:
|
||||
|
||||
- **Truong Phung** (DEV Community, May 29) published a comprehensive production playbook for AI-assisted development, referencing Spec Kit (see The Production Playbook Pattern below). [\[dev.to\]](https://dev.to/truongpx396/building-production-grade-fullstack-products-with-ai-coding-agents-a-practical-playbook-2idd)
|
||||
- **Mehul Gupta** (Medium, May 17) called Spec Kit "an operating system for AI-assisted software engineering." [\[medium.com\]](https://medium.com/data-science-in-your-pocket/what-is-github-spec-kit-bye-bye-vibe-coding-37efbaa32880)
|
||||
- **Kento IKEDA** (DEV Community / AWS Builders, May 2) examined the emerging three-layer pattern for AI agent instructions (AGENTS.md, SKILL.md, DESIGN.md), referencing Spec Kit's approach. [\[dev.to\]](https://dev.to/aws-builders/agentsmd-skillmd-designmd-how-ai-instructions-split-into-three-layers-d0g)
|
||||
- **PyShine** (May 13) published a detailed guide covering the 6-step workflow, 30+ integrations, and 60+ extensions. [\[pyshine.com\]](https://pyshine.com/GitHub-Spec-Kit-Spec-Driven-Development/)
|
||||
- **DeployHQ** (Alex M, May 13) examined the "deployment gap" — Spec Kit ends at code, Workspaces ends at PR — and showed how to wire DeployHQ into the post-merge step. [\[deployhq.com\]](https://www.deployhq.com/blog/spec-kit-copilot-workspaces-deployment)
|
||||
- **spec-coding.dev** (May 11) examined five practical SDD patterns shared by OpenSpec, Superpowers, and Spec Kit. [\[spec-coding.dev\]](https://spec-coding.dev/blog/spec-driven-development-tools-openspec-spec-kit-superpowers)
|
||||
- **kiadev.net** (Ignaty Kashnitsky, May 9) published two articles: a detailed technical protocol and a 9-tool comparison recommending Spec Kit as a "portable, community-driven starting point." [\[kiadev.net\]](https://www.kiadev.net/news/2026-05-09-github-spec-kit-sdd-toolkit)
|
||||
|
||||
Coverage also appeared on WinBuzzer, Let's Data Science, Openflows, AI in Plain English (Medium), Artiverse, KnightLi Blog (multilingual EN/CN/JP/ES), and fundesk.io.
|
||||
|
||||
### Community Growth by the Numbers
|
||||
|
||||
| Metric | Start of May | End of May | Change |
|
||||
| --- | --- | --- | --- |
|
||||
| GitHub stars | 92,038 | 106,951 | +14,913 (+16%) |
|
||||
| Forks | ~8,000 | 9,464 | +~1,500 |
|
||||
| Contributors | — | 217 | — |
|
||||
| Releases (total) | 135 | 152 | +17 (incl. 3 late-April) |
|
||||
| Community extensions | 92 | 105 | +13 |
|
||||
| Community presets | 18 | 21 | +3 |
|
||||
| Discussions (open) | ~400 | 422 | +~22 |
|
||||
|
||||
## SDD Ecosystem & Industry Trends
|
||||
|
||||
### The Analyst View
|
||||
|
||||
The Futurum Group's **Mitch Ashley** provided the most significant analyst framing of SDD to date on DevOps.com: "GitHub's Spec Kit signals AI-assisted coding is shifting from prompts to durable, versioned specifications. Vendors are competing to own the artifact that governs intent across Copilot, Claude Code, and Gemini CLI." He warned that "verification at each checkpoint cannot be deferred to the agent producing it" — echoing the project's own emphasis on human oversight at phase boundaries. [\[devops.com\]](https://devops.com/githubs-spec-kit-puts-the-spec-back-in-software-development/)
|
||||
|
||||
### The Production Playbook Pattern
|
||||
|
||||
**Truong Phung's** 61-minute production playbook represented a new level of depth in community content. Rather than reviewing Spec Kit as a tool, Phung treated SDD as a given and built a comprehensive guide around the **Spec → Plan → Code → Verify loop**, with Spec Kit and Superpowers as the reference implementations. His seven opening truths — "the bottleneck moved from typing to thinking," "context engineering > prompt engineering," and "the PR is the unit of work, not the ticket" — capture the emerging practitioner consensus around structured AI development. [\[dev.to\]](https://dev.to/truongpx396/building-production-grade-fullstack-products-with-ai-coding-agents-a-practical-playbook-2idd)
|
||||
|
||||
### Competitive Landscape
|
||||
|
||||
The **MarkTechPost comparison** of nine SDD tools called Spec Kit "the most community-adopted open-source option," while positioning competitors along distinct axes: **Kiro** (integrated IDE with EARS-based specs and agent hooks), **BMAD-METHOD** (~48K stars, 12+ specialized agents), **GSD** (~64K stars, lean meta-prompting), **Augment Code** (context engine for 400K+ files, not a spec authoring tool), **OpenSpec** (~52K stars, change accountability and audit trails), and **Tessl** (spec registry with 10K+ library specs). [\[marktechpost.com\]](https://www.marktechpost.com/2026/05/08/9-best-ai-tools-for-spec-driven-development-in-2026-kiro-bmad-gsd-and-more-compare/)
|
||||
|
||||
With 107K stars at month's end, Spec Kit is the **only spec-driven development tool in the top 100 most-starred repositories on GitHub** — none of the competitors above are close to the 100K threshold. The broader top-100 list includes AI-adjacent projects like agentic skills frameworks (obra/superpowers at 212K, anthropics/skills at 143K), agent harness tools, and LLM inference engines, but Spec Kit is the only one built around a spec-first development workflow. [\[github.com\]](https://github.com/search?q=stars%3A%3E100000&type=repositories&s=stars&o=desc)
|
||||
|
||||
## Roadmap
|
||||
|
||||
Areas under discussion or in progress for future development:
|
||||
|
||||
- **CLI architecture cleanup** — the `__init__.py` decomposition (4/8 complete) continues toward a modular command structure. This internal cleanup improves contributor onboarding and test isolation. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **Spec lifecycle management** — spec drift and context rot remain the most cited concern across articles (DevOps.com, DeployHQ, テックオーシャン). The clarify re-validation (#2715) and reconcile extensions are incremental steps; a more comprehensive solution is expected. [\[devops.com\]](https://devops.com/githubs-spec-kit-puts-the-spec-back-in-software-development/)
|
||||
- **Multi-agent workflows** — multi-install support (#2389) was the most-requested feature. The next frontier is orchestrating multiple agents across phases, a pattern the community's MAQA, Fleet, and Conduct extensions already explore. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **Catalog maturity** — catalog discovery CLI (v0.8.3), agentic submissions (v0.8.13), and GITHUB_TOKEN auth (v0.8.2) are building toward a package-manager experience. As the catalog grows past 100 entries, curation and quality signals become critical. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
- **Experience simplification** — the deployment gap (DeployHQ), ceremony overhead for small tasks (テックオーシャン, spec-coding.dev), and verbose output (Thoughtworks Radar) continue as open concerns. The lean preset, TinySpec extension, and workflow engine provide answers; discoverability of these options remains an opportunity. [\[deployhq.com\]](https://www.deployhq.com/blog/spec-kit-copilot-workspaces-deployment)
|
||||
- **Toward a stable release** — fourteen releases in one month reflects pre-1.0 momentum. The git extension default-off notice (#2432, gated at v0.10.0) and the `--no-git` deprecation (removal at v0.10.0) signal a path toward API stabilization. [\[github.com\]](https://github.com/github/spec-kit/releases)
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "specify-cli"
|
||||
version = "0.9.0"
|
||||
version = "0.8.18"
|
||||
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
@@ -40,7 +40,6 @@ packages = ["src/specify_cli"]
|
||||
"scripts/powershell" = "specify_cli/core_pack/scripts/powershell"
|
||||
# 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"
|
||||
# Bundled workflows (auto-installed during `specify init`)
|
||||
"workflows/speckit" = "specify_cli/core_pack/workflows/speckit"
|
||||
# Bundled presets (installable via `specify preset add <name>` or `specify init --preset <name>`)
|
||||
|
||||
@@ -304,72 +304,6 @@ def load_init_options(project_path: Path) -> dict[str, Any]:
|
||||
return {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Agent-context extension config helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_AGENT_CTX_EXT_CONFIG = (
|
||||
Path(".specify") / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
)
|
||||
|
||||
|
||||
def _load_agent_context_config(project_root: Path) -> dict[str, Any]:
|
||||
"""Load the agent-context extension config, returning defaults on failure."""
|
||||
from .integrations.base import IntegrationBase
|
||||
|
||||
defaults: dict[str, Any] = {
|
||||
"context_file": "",
|
||||
"context_markers": {
|
||||
"start": IntegrationBase.CONTEXT_MARKER_START,
|
||||
"end": IntegrationBase.CONTEXT_MARKER_END,
|
||||
},
|
||||
}
|
||||
path = project_root / _AGENT_CTX_EXT_CONFIG
|
||||
if not path.exists():
|
||||
return defaults
|
||||
try:
|
||||
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, yaml.YAMLError):
|
||||
return defaults
|
||||
if not isinstance(raw, dict):
|
||||
return defaults
|
||||
return raw
|
||||
|
||||
|
||||
def _save_agent_context_config(
|
||||
project_root: Path, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Persist *config* to the agent-context extension config file."""
|
||||
path = project_root / _AGENT_CTX_EXT_CONFIG
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(yaml.safe_dump(config, default_flow_style=False, sort_keys=False), encoding="utf-8")
|
||||
|
||||
|
||||
def _update_agent_context_config_file(
|
||||
project_root: Path,
|
||||
context_file: str | None,
|
||||
*,
|
||||
preserve_markers: bool = True,
|
||||
) -> None:
|
||||
"""Update the agent-context extension config with *context_file*.
|
||||
|
||||
When *preserve_markers* is True (default), any existing
|
||||
``context_markers`` values are kept unchanged so user customisations
|
||||
survive integration changes and reinit. When False, the default
|
||||
markers are written unconditionally.
|
||||
"""
|
||||
from .integrations.base import IntegrationBase
|
||||
|
||||
cfg = _load_agent_context_config(project_root)
|
||||
cfg["context_file"] = context_file or ""
|
||||
if not preserve_markers or not isinstance(cfg.get("context_markers"), dict):
|
||||
cfg["context_markers"] = {
|
||||
"start": IntegrationBase.CONTEXT_MARKER_START,
|
||||
"end": IntegrationBase.CONTEXT_MARKER_END,
|
||||
}
|
||||
_save_agent_context_config(project_root, cfg)
|
||||
|
||||
|
||||
def _get_skills_dir(project_path: Path, selected_ai: str) -> Path:
|
||||
"""Resolve the agent-specific skills directory.
|
||||
|
||||
@@ -715,31 +649,13 @@ def _refresh_init_options_speckit_version(project_root: Path) -> None:
|
||||
|
||||
|
||||
def _clear_init_options_for_integration(project_root: Path, integration_key: str) -> None:
|
||||
"""Clear active integration keys from init-options.json when they match.
|
||||
|
||||
Also clears ``context_file`` from the agent-context extension config so
|
||||
no stale path is left behind when the integration is uninstalled.
|
||||
"""
|
||||
"""Clear active integration keys from init-options.json when they match."""
|
||||
opts = load_init_options(project_root)
|
||||
has_legacy_context_keys = ("context_file" in opts) or ("context_markers" in opts)
|
||||
# Remove legacy fields that older versions may have written.
|
||||
opts.pop("context_file", None)
|
||||
opts.pop("context_markers", None)
|
||||
|
||||
if opts.get("integration") == integration_key or opts.get("ai") == integration_key:
|
||||
opts.pop("integration", None)
|
||||
opts.pop("ai", None)
|
||||
opts.pop("ai_skills", None)
|
||||
save_init_options(project_root, opts)
|
||||
# Clear context_file in the extension config if it already exists.
|
||||
# Avoid creating the config (and parent dirs) in projects where the
|
||||
# agent-context extension was never installed.
|
||||
ext_cfg_path = project_root / _AGENT_CTX_EXT_CONFIG
|
||||
if ext_cfg_path.exists():
|
||||
_update_agent_context_config_file(
|
||||
project_root, "", preserve_markers=True
|
||||
)
|
||||
elif has_legacy_context_keys:
|
||||
opts.pop("context_file", None)
|
||||
save_init_options(project_root, opts)
|
||||
|
||||
|
||||
@@ -1184,23 +1100,12 @@ def _update_init_options_for_integration(
|
||||
integration: Any,
|
||||
script_type: str | None = None,
|
||||
) -> None:
|
||||
"""Update init-options.json and the agent-context extension config to
|
||||
reflect *integration* as the active one.
|
||||
|
||||
``context_file`` and ``context_markers`` are stored in the agent-context
|
||||
extension config (``.specify/extensions/agent-context/agent-context-config.yml``),
|
||||
not in ``init-options.json``. Existing user-customised markers are
|
||||
always preserved when the config already exists; invalid marker values
|
||||
are silently ignored at runtime by ``_resolve_context_markers()`` which
|
||||
falls back to the class-level defaults.
|
||||
"""
|
||||
"""Update ``init-options.json`` to reflect *integration* as the active one."""
|
||||
from .integrations.base import SkillsIntegration
|
||||
opts = load_init_options(project_root)
|
||||
opts["integration"] = integration.key
|
||||
opts["ai"] = integration.key
|
||||
# Remove legacy fields if they were written by an older version.
|
||||
opts.pop("context_file", None)
|
||||
opts.pop("context_markers", None)
|
||||
opts["context_file"] = integration.context_file
|
||||
opts["speckit_version"] = get_speckit_version()
|
||||
if script_type:
|
||||
opts["script"] = script_type
|
||||
@@ -1208,25 +1113,6 @@ def _update_init_options_for_integration(
|
||||
opts["ai_skills"] = True
|
||||
else:
|
||||
opts.pop("ai_skills", None)
|
||||
|
||||
# Update the agent-context extension config BEFORE init-options.json
|
||||
# so a failure here doesn't leave init-options partially updated.
|
||||
ext_cfg_path = project_root / _AGENT_CTX_EXT_CONFIG
|
||||
if ext_cfg_path.exists():
|
||||
_update_agent_context_config_file(
|
||||
project_root,
|
||||
integration.context_file,
|
||||
preserve_markers=True,
|
||||
)
|
||||
elif integration.context_file:
|
||||
# Extension config doesn't exist yet (extension not installed).
|
||||
# Write defaults so scripts have something to read.
|
||||
_update_agent_context_config_file(
|
||||
project_root,
|
||||
integration.context_file,
|
||||
preserve_markers=False,
|
||||
)
|
||||
|
||||
save_init_options(project_root, opts)
|
||||
|
||||
|
||||
@@ -3082,43 +2968,6 @@ def extension_add(
|
||||
manager = ExtensionManager(project_root)
|
||||
speckit_version = get_speckit_version()
|
||||
|
||||
# Prompt for URL-based installs BEFORE the spinner so the user can
|
||||
# actually see and respond to the confirmation (the Rich status
|
||||
# spinner overwrites the typer.confirm prompt line, making it appear
|
||||
# as though the command is hung).
|
||||
# Guard with ``not dev`` so that --dev + --from does not show a
|
||||
# confusing confirmation for a URL that will be ignored.
|
||||
if from_url and not dev:
|
||||
from urllib.parse import urlparse
|
||||
from rich.markup import escape as _escape_markup
|
||||
|
||||
parsed = urlparse(from_url)
|
||||
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
|
||||
|
||||
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
|
||||
console.print("[red]Error:[/red] URL must use HTTPS for security.")
|
||||
console.print("HTTP is only allowed for localhost URLs.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
safe_url = _escape_markup(from_url)
|
||||
|
||||
# Warn about untrusted sources — default-deny confirmation
|
||||
console.print()
|
||||
console.print(Panel(
|
||||
f"[bold]You are installing an extension from an external URL that is not\n"
|
||||
f"listed in any of your configured extension catalogs.[/bold]\n\n"
|
||||
f"URL: {safe_url}\n\n"
|
||||
f"Only install extensions from sources you trust.",
|
||||
title="[bold yellow]⚠ Untrusted Source[/bold yellow]",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
))
|
||||
console.print()
|
||||
confirm = typer.confirm("Continue with installation?", default=False)
|
||||
if not confirm:
|
||||
console.print("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
try:
|
||||
with console.status(f"[cyan]Installing extension: {extension}[/cyan]"):
|
||||
if dev:
|
||||
@@ -3141,9 +2990,37 @@ def extension_add(
|
||||
|
||||
elif from_url:
|
||||
# Install from URL (ZIP file)
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from urllib.parse import urlparse
|
||||
|
||||
console.print(f"Downloading from {safe_url}...")
|
||||
# Validate URL
|
||||
parsed = urlparse(from_url)
|
||||
is_localhost = parsed.hostname in ("localhost", "127.0.0.1", "::1")
|
||||
|
||||
if parsed.scheme != "https" and not (parsed.scheme == "http" and is_localhost):
|
||||
console.print("[red]Error:[/red] URL must use HTTPS for security.")
|
||||
console.print("HTTP is only allowed for localhost URLs.")
|
||||
raise typer.Exit(1)
|
||||
|
||||
# Warn about untrusted sources — default-deny confirmation
|
||||
console.print()
|
||||
console.print(Panel(
|
||||
f"[bold]You are installing an extension from an external URL that is not\n"
|
||||
f"listed in any of your configured extension catalogs.[/bold]\n\n"
|
||||
f"URL: {from_url}\n\n"
|
||||
f"Only install extensions from sources you trust.",
|
||||
title="[bold yellow]⚠ Untrusted Source[/bold yellow]",
|
||||
border_style="yellow",
|
||||
padding=(1, 2),
|
||||
))
|
||||
console.print()
|
||||
confirm = typer.confirm("Continue with installation?", default=False)
|
||||
if not confirm:
|
||||
console.print("Cancelled")
|
||||
raise typer.Exit(0)
|
||||
|
||||
console.print(f"Downloading from {from_url}...")
|
||||
|
||||
# Download ZIP to temp location
|
||||
download_dir = project_root / ".specify" / "extensions" / ".cache" / "downloads"
|
||||
@@ -3160,7 +3037,7 @@ def extension_add(
|
||||
# Install from downloaded ZIP
|
||||
manifest = manager.install_from_zip(zip_path, speckit_version, priority=priority)
|
||||
except urllib.error.URLError as e:
|
||||
console.print(f"[red]Error:[/red] Failed to download from {safe_url}: {e}")
|
||||
console.print(f"[red]Error:[/red] Failed to download from {from_url}: {e}")
|
||||
raise typer.Exit(1)
|
||||
finally:
|
||||
# Clean up downloaded ZIP
|
||||
|
||||
@@ -374,15 +374,8 @@ class CommandRegistrar:
|
||||
|
||||
body = body.replace("{ARGS}", "$ARGUMENTS").replace("__AGENT__", agent_name)
|
||||
|
||||
# Resolve __CONTEXT_FILE__ from the agent-context extension config.
|
||||
# Fall back to init-options.json for projects that haven't migrated.
|
||||
# Local import: _load_agent_context_config lives in __init__.py which
|
||||
# imports agents.py, so a top-level import would be circular.
|
||||
from . import _load_agent_context_config
|
||||
ac_cfg = _load_agent_context_config(project_root)
|
||||
context_file = ac_cfg.get("context_file") or ""
|
||||
if not context_file:
|
||||
context_file = init_opts.get("context_file") or ""
|
||||
# Resolve __CONTEXT_FILE__ from init-options
|
||||
context_file = init_opts.get("context_file") or ""
|
||||
body = body.replace("__CONTEXT_FILE__", context_file)
|
||||
|
||||
return CommandRegistrar.rewrite_project_relative_paths(body)
|
||||
|
||||
@@ -153,7 +153,6 @@ def register(app: typer.Typer) -> None:
|
||||
_install_shared_infra_or_exit,
|
||||
_parse_integration_options,
|
||||
_print_cli_warning,
|
||||
_update_agent_context_config_file,
|
||||
_write_integration_json,
|
||||
ensure_executable_scripts,
|
||||
save_init_options,
|
||||
@@ -395,7 +394,6 @@ def register(app: typer.Typer) -> None:
|
||||
("constitution", "Constitution setup"),
|
||||
("git", "Install git extension"),
|
||||
("workflow", "Install bundled workflow"),
|
||||
("agent-context", "Install agent-context extension"),
|
||||
("final", "Finalize"),
|
||||
]:
|
||||
tracker.add(key, label)
|
||||
@@ -537,10 +535,13 @@ def register(app: typer.Typer) -> None:
|
||||
sanitized_wf = str(wf_err).replace('\n', ' ').strip()
|
||||
tracker.error("workflow", f"install failed: {sanitized_wf[:120]}")
|
||||
|
||||
ensure_executable_scripts(project_path, tracker=tracker)
|
||||
|
||||
init_opts = {
|
||||
"ai": selected_ai,
|
||||
"integration": resolved_integration.key,
|
||||
"branch_numbering": branch_numbering or "sequential",
|
||||
"context_file": resolved_integration.context_file,
|
||||
"here": here,
|
||||
"script": selected_script,
|
||||
"speckit_version": get_speckit_version(),
|
||||
@@ -550,47 +551,6 @@ def register(app: typer.Typer) -> None:
|
||||
init_opts["ai_skills"] = True
|
||||
save_init_options(project_path, init_opts)
|
||||
|
||||
# --- agent-context extension (bundled, auto-installed) ---
|
||||
# Installed after init-options.json is written so that skill
|
||||
# registration can read ai_skills + integration key.
|
||||
try:
|
||||
from ..extensions import ExtensionManager as _ExtMgr
|
||||
bundled_ac = _locate_bundled_extension("agent-context")
|
||||
if bundled_ac:
|
||||
ac_mgr = _ExtMgr(project_path)
|
||||
if ac_mgr.registry.is_installed("agent-context"):
|
||||
tracker.complete("agent-context", "already installed")
|
||||
else:
|
||||
ac_mgr.install_from_directory(
|
||||
bundled_ac, get_speckit_version()
|
||||
)
|
||||
tracker.complete("agent-context", "extension installed")
|
||||
else:
|
||||
from ..extensions import REINSTALL_COMMAND as _ac_reinstall
|
||||
tracker.error(
|
||||
"agent-context",
|
||||
f"bundled extension not found — installation may be "
|
||||
f"incomplete. Run: {_ac_reinstall}",
|
||||
)
|
||||
except Exception as ac_err:
|
||||
sanitized_ac = str(ac_err).replace('\n', ' ').strip()
|
||||
tracker.error(
|
||||
"agent-context",
|
||||
f"extension install failed: {sanitized_ac[:120]}",
|
||||
)
|
||||
|
||||
# Write context_file to the agent-context extension config
|
||||
# AFTER the extension install (which copies the template config
|
||||
# with an empty context_file).
|
||||
if resolved_integration.context_file:
|
||||
_update_agent_context_config_file(
|
||||
project_path,
|
||||
resolved_integration.context_file,
|
||||
preserve_markers=True,
|
||||
)
|
||||
|
||||
ensure_executable_scripts(project_path, tracker=tracker)
|
||||
|
||||
if preset:
|
||||
try:
|
||||
from ..presets import PresetManager, PresetCatalog, PresetError
|
||||
|
||||
@@ -13,7 +13,6 @@ Provides:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
@@ -550,91 +549,6 @@ class IntegrationBase(ABC):
|
||||
lines.append(f"at {plan_path}")
|
||||
return "\n".join(lines)
|
||||
|
||||
@staticmethod
|
||||
def _agent_context_extension_enabled(project_root: Path) -> bool:
|
||||
"""Return whether the bundled ``agent-context`` extension is enabled.
|
||||
|
||||
The extension is the single source of truth for managing coding
|
||||
agent context/instruction files (e.g. ``CLAUDE.md``,
|
||||
``.github/copilot-instructions.md``).
|
||||
|
||||
Returns ``True`` (enabled) when:
|
||||
- the extension registry does not exist (legacy project, backwards
|
||||
compatibility), or
|
||||
- the registry has no ``agent-context`` entry (older project layout
|
||||
predating the extension), or
|
||||
- the entry is present and not explicitly disabled.
|
||||
|
||||
Returns ``False`` only when an entry exists with ``enabled: false``.
|
||||
"""
|
||||
registry_path = (
|
||||
project_root / ".specify" / "extensions" / ".registry"
|
||||
)
|
||||
if not registry_path.exists():
|
||||
return True
|
||||
try:
|
||||
data = json.loads(registry_path.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError, UnicodeError):
|
||||
return True
|
||||
if not isinstance(data, dict):
|
||||
return True
|
||||
extensions = data.get("extensions")
|
||||
if not isinstance(extensions, dict):
|
||||
return True
|
||||
entry = extensions.get("agent-context")
|
||||
if not isinstance(entry, dict):
|
||||
return True
|
||||
return entry.get("enabled", True) is not False
|
||||
|
||||
def _resolve_context_markers(self, project_root: Path) -> tuple[str, str]:
|
||||
"""Return the (start, end) context markers to use for *project_root*.
|
||||
|
||||
Reads ``context_markers.start`` / ``context_markers.end`` from the
|
||||
agent-context extension config
|
||||
(``.specify/extensions/agent-context/agent-context-config.yml``)
|
||||
when present. Falls back to the class-level constants
|
||||
``CONTEXT_MARKER_START`` / ``CONTEXT_MARKER_END`` when the file is
|
||||
missing, the section is absent, or the values are not non-empty
|
||||
strings.
|
||||
"""
|
||||
from .._console import console # local import to avoid cycles
|
||||
|
||||
start = self.CONTEXT_MARKER_START
|
||||
end = self.CONTEXT_MARKER_END
|
||||
config_path = (
|
||||
project_root
|
||||
/ ".specify"
|
||||
/ "extensions"
|
||||
/ "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
try:
|
||||
raw = config_path.read_text(encoding="utf-8")
|
||||
cfg = yaml.safe_load(raw)
|
||||
except (OSError, UnicodeError, ValueError, yaml.YAMLError):
|
||||
return start, end
|
||||
markers = cfg.get("context_markers") if isinstance(cfg, dict) else None
|
||||
if isinstance(markers, dict):
|
||||
cm_start = markers.get("start")
|
||||
cm_end = markers.get("end")
|
||||
s_valid = isinstance(cm_start, str) and cm_start
|
||||
e_valid = isinstance(cm_end, str) and cm_end
|
||||
if not s_valid and cm_start is not None:
|
||||
console.print(
|
||||
f"[yellow]agent-context: ignoring invalid context_markers.start "
|
||||
f"({cm_start!r}), using default[/yellow]"
|
||||
)
|
||||
if not e_valid and cm_end is not None:
|
||||
console.print(
|
||||
f"[yellow]agent-context: ignoring invalid context_markers.end "
|
||||
f"({cm_end!r}), using default[/yellow]"
|
||||
)
|
||||
if s_valid:
|
||||
start = cm_start # type: ignore[assignment]
|
||||
if e_valid:
|
||||
end = cm_end # type: ignore[assignment]
|
||||
return start, end
|
||||
|
||||
def upsert_context_section(
|
||||
self,
|
||||
project_root: Path,
|
||||
@@ -643,54 +557,34 @@ class IntegrationBase(ABC):
|
||||
"""Create or update the managed section in the agent context file.
|
||||
|
||||
If the context file does not exist it is created with just the
|
||||
managed section. If it exists, the content between the configured
|
||||
start/end markers (default ``<!-- SPECKIT START -->`` /
|
||||
``<!-- SPECKIT END -->``) is replaced, or appended when no markers
|
||||
are found. Markers are read from the agent-context extension config
|
||||
(``.specify/extensions/agent-context/agent-context-config.yml``)
|
||||
when present, falling back to the class-level constants.
|
||||
managed section. If it exists, the content between
|
||||
``<!-- SPECKIT START -->`` and ``<!-- SPECKIT END -->`` markers
|
||||
is replaced (or appended when no markers are found).
|
||||
|
||||
Returns the path to the context file, or ``None`` when
|
||||
``context_file`` is not set or the ``agent-context`` extension is
|
||||
disabled.
|
||||
``context_file`` is not set.
|
||||
"""
|
||||
if not self.context_file:
|
||||
return None
|
||||
|
||||
if not self._agent_context_extension_enabled(project_root):
|
||||
return None
|
||||
|
||||
from .._console import console # local import to avoid cycles
|
||||
|
||||
console.print(
|
||||
"[yellow]Deprecation:[/yellow] Inline agent-context updates during "
|
||||
"integration setup will be disabled in v0.12.0. Context file "
|
||||
"management has moved to the bundled [bold]agent-context[/bold] "
|
||||
"extension. Run [cyan]specify extension disable agent-context[/cyan] "
|
||||
"to opt out early.",
|
||||
highlight=False,
|
||||
)
|
||||
|
||||
marker_start, marker_end = self._resolve_context_markers(project_root)
|
||||
|
||||
ctx_path = project_root / self.context_file
|
||||
section = (
|
||||
f"{marker_start}\n"
|
||||
f"{self.CONTEXT_MARKER_START}\n"
|
||||
f"{self._build_context_section(plan_path)}\n"
|
||||
f"{marker_end}\n"
|
||||
f"{self.CONTEXT_MARKER_END}\n"
|
||||
)
|
||||
|
||||
if ctx_path.exists():
|
||||
content = ctx_path.read_text(encoding="utf-8-sig")
|
||||
start_idx = content.find(marker_start)
|
||||
start_idx = content.find(self.CONTEXT_MARKER_START)
|
||||
end_idx = content.find(
|
||||
marker_end,
|
||||
self.CONTEXT_MARKER_END,
|
||||
start_idx if start_idx != -1 else 0,
|
||||
)
|
||||
|
||||
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
||||
# Replace existing section (include the end marker + newline)
|
||||
end_of_marker = end_idx + len(marker_end)
|
||||
end_of_marker = end_idx + len(self.CONTEXT_MARKER_END)
|
||||
# Consume trailing line ending (CRLF or LF)
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
||||
end_of_marker += 1
|
||||
@@ -702,7 +596,7 @@ class IntegrationBase(ABC):
|
||||
new_content = content[:start_idx] + section
|
||||
elif end_idx != -1:
|
||||
# Corrupted: end marker without start — replace BOF through end marker
|
||||
end_of_marker = end_idx + len(marker_end)
|
||||
end_of_marker = end_idx + len(self.CONTEXT_MARKER_END)
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\r":
|
||||
end_of_marker += 1
|
||||
if end_of_marker < len(content) and content[end_of_marker] == "\n":
|
||||
@@ -736,27 +630,20 @@ class IntegrationBase(ABC):
|
||||
"""Remove the managed section from the agent context file.
|
||||
|
||||
Returns ``True`` if the section was found and removed. If the
|
||||
file becomes empty (or whitespace-only) after removal it is deleted.
|
||||
Markers are read from the agent-context extension config
|
||||
(``.specify/extensions/agent-context/agent-context-config.yml``)
|
||||
when present, falling back to the class-level constants.
|
||||
file becomes empty (or whitespace-only) after removal it is
|
||||
deleted.
|
||||
"""
|
||||
if not self.context_file:
|
||||
return False
|
||||
|
||||
if not self._agent_context_extension_enabled(project_root):
|
||||
return False
|
||||
|
||||
ctx_path = project_root / self.context_file
|
||||
if not ctx_path.exists():
|
||||
return False
|
||||
|
||||
marker_start, marker_end = self._resolve_context_markers(project_root)
|
||||
|
||||
content = ctx_path.read_text(encoding="utf-8-sig")
|
||||
start_idx = content.find(marker_start)
|
||||
start_idx = content.find(self.CONTEXT_MARKER_START)
|
||||
end_idx = content.find(
|
||||
marker_end,
|
||||
self.CONTEXT_MARKER_END,
|
||||
start_idx if start_idx != -1 else 0,
|
||||
)
|
||||
|
||||
@@ -767,7 +654,7 @@ class IntegrationBase(ABC):
|
||||
return False
|
||||
|
||||
removal_start = start_idx
|
||||
removal_end = end_idx + len(marker_end)
|
||||
removal_end = end_idx + len(self.CONTEXT_MARKER_END)
|
||||
|
||||
# Consume trailing line ending (CRLF or LF)
|
||||
if removal_end < len(content) and content[removal_end] == "\r":
|
||||
|
||||
@@ -74,9 +74,7 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
- All file paths must be absolute.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints.
|
||||
|
||||
3. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
|
||||
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
|
||||
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
|
||||
- Only ask about information that materially changes checklist content
|
||||
- Be skipped individually if already unambiguous in `$ARGUMENTS`
|
||||
@@ -108,13 +106,13 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
|
||||
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted follow‑ups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
|
||||
|
||||
4. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
|
||||
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
|
||||
- Derive checklist theme (e.g., security, review, deploy, ux)
|
||||
- Consolidate explicit must-have items mentioned by user
|
||||
- Map focus selections to category scaffolding
|
||||
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
|
||||
|
||||
5. **Load feature context**: Read from FEATURE_DIR:
|
||||
4. **Load feature context**: Read from FEATURE_DIR:
|
||||
- spec.md: Feature requirements and scope
|
||||
- plan.md (if exists): Technical details, dependencies
|
||||
- tasks.md (if exists): Implementation tasks
|
||||
@@ -125,7 +123,7 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
- Use progressive disclosure: add follow-on retrieval only if gaps detected
|
||||
- If source docs are large, generate interim summary items instead of embedding raw text
|
||||
|
||||
6. **Generate checklist** - Create "Unit Tests for Requirements":
|
||||
5. **Generate checklist** - Create "Unit Tests for Requirements":
|
||||
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
|
||||
- Generate unique checklist filename:
|
||||
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
|
||||
@@ -243,9 +241,9 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
|
||||
- ✅ "Does the spec define [missing aspect]?"
|
||||
|
||||
7. **Structure Reference**: Generate the checklist following the canonical template in `templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
|
||||
6. **Structure Reference**: Generate the checklist following the canonical template in `templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
|
||||
|
||||
8. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize:
|
||||
7. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize:
|
||||
- Focus areas selected
|
||||
- Depth level
|
||||
- Actor/timing
|
||||
|
||||
@@ -66,9 +66,7 @@ Execution steps:
|
||||
- If JSON parsing fails, abort and instruct user to re-run `__SPECKIT_COMMAND_SPECIFY__` or verify feature branch environment.
|
||||
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
|
||||
2. **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints.
|
||||
|
||||
3. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
|
||||
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
|
||||
|
||||
Functional Scope & Behavior:
|
||||
- Core user goals & success criteria
|
||||
@@ -124,7 +122,7 @@ Execution steps:
|
||||
- Clarification would not materially change implementation or validation strategy
|
||||
- Information is better deferred to planning phase (note internally)
|
||||
|
||||
4. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
|
||||
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
|
||||
- Maximum of 5 total questions across the whole session.
|
||||
- Each question must be answerable with EITHER:
|
||||
- A short multiple‑choice selection (2–5 distinct, mutually exclusive options), OR
|
||||
@@ -135,7 +133,7 @@ Execution steps:
|
||||
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
|
||||
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
|
||||
|
||||
5. Sequential questioning loop (interactive):
|
||||
4. Sequential questioning loop (interactive):
|
||||
- Present EXACTLY ONE question at a time.
|
||||
- For multiple‑choice questions:
|
||||
- **Analyze all options** and determine the **most suitable option** based on:
|
||||
@@ -171,7 +169,7 @@ Execution steps:
|
||||
- Never reveal future queued questions in advance.
|
||||
- If no valid questions exist at start, immediately report no critical ambiguities.
|
||||
|
||||
6. Integration after EACH accepted answer (incremental update approach):
|
||||
5. Integration after EACH accepted answer (incremental update approach):
|
||||
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
|
||||
- For the first integrated answer in this session:
|
||||
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
|
||||
@@ -189,7 +187,7 @@ Execution steps:
|
||||
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
|
||||
- Keep each inserted clarification minimal and testable (avoid narrative drift).
|
||||
|
||||
7. Validation (performed after EACH write plus final pass):
|
||||
6. Validation (performed after EACH write plus final pass):
|
||||
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
|
||||
- Total asked (accepted) questions ≤ 5.
|
||||
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
|
||||
@@ -197,9 +195,9 @@ Execution steps:
|
||||
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
|
||||
- Terminology consistency: same canonical term used across all updated sections.
|
||||
|
||||
8. Write the updated spec back to `FEATURE_SPEC`.
|
||||
7. Write the updated spec back to `FEATURE_SPEC`.
|
||||
|
||||
9. **Re-validate Spec Quality Checklist** (if it exists):
|
||||
8. **Re-validate Spec Quality Checklist** (if it exists):
|
||||
- Check if `FEATURE_DIR/checklists/requirements.md` exists.
|
||||
- If it does NOT exist, skip this step silently.
|
||||
- If it exists:
|
||||
|
||||
@@ -109,9 +109,7 @@ Given that feature description, do this:
|
||||
|
||||
4. Load `templates/spec-template.md` to understand required sections.
|
||||
|
||||
5. **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints.
|
||||
|
||||
6. Follow this execution flow:
|
||||
5. Follow this execution flow:
|
||||
1. Parse user description from arguments
|
||||
If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
|
||||
@@ -63,7 +63,6 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
2. **Load design documents**: Read from FEATURE_DIR:
|
||||
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
|
||||
- **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md (test scenarios)
|
||||
- **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints
|
||||
- Note: Not all projects have all documents. Generate tasks based on what's available.
|
||||
|
||||
3. **Execute task generation workflow**:
|
||||
|
||||
@@ -51,7 +51,6 @@ You **MUST** consider the user input before proceeding (if not empty).
|
||||
## Outline
|
||||
|
||||
1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
|
||||
1. **IF EXISTS**: Load `/memory/constitution.md` for project principles and governance constraints.
|
||||
1. From the executed script, extract the path to **tasks**.
|
||||
1. Get the Git remote by running:
|
||||
|
||||
|
||||
@@ -1,455 +0,0 @@
|
||||
"""Tests for the bundled ``agent-context`` extension and related plumbing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from specify_cli import (
|
||||
_load_agent_context_config,
|
||||
_save_agent_context_config,
|
||||
load_init_options,
|
||||
save_init_options,
|
||||
)
|
||||
from specify_cli.integrations.base import IntegrationBase
|
||||
from specify_cli.integrations.claude import ClaudeIntegration
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
EXT_DIR = PROJECT_ROOT / "extensions" / "agent-context"
|
||||
|
||||
|
||||
def _write_ext_config(project_root: Path, **overrides: object) -> None:
|
||||
"""Write a minimal agent-context extension config."""
|
||||
cfg: dict = {
|
||||
"context_file": overrides.get("context_file", ""),
|
||||
"context_markers": overrides.get(
|
||||
"context_markers",
|
||||
{
|
||||
"start": IntegrationBase.CONTEXT_MARKER_START,
|
||||
"end": IntegrationBase.CONTEXT_MARKER_END,
|
||||
},
|
||||
),
|
||||
}
|
||||
_save_agent_context_config(project_root, cfg)
|
||||
|
||||
|
||||
# ── Bundled extension layout ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtensionLayout:
|
||||
"""The bundled agent-context extension ships a complete package."""
|
||||
|
||||
def test_extension_yml_exists(self):
|
||||
assert (EXT_DIR / "extension.yml").is_file()
|
||||
|
||||
def test_extension_yml_has_required_fields(self):
|
||||
manifest = yaml.safe_load((EXT_DIR / "extension.yml").read_text())
|
||||
assert manifest["extension"]["id"] == "agent-context"
|
||||
assert manifest["extension"]["name"] == "Coding Agent Context"
|
||||
assert manifest["extension"]["author"] == "spec-kit-core"
|
||||
# Provides at least the manual update command
|
||||
commands = {c["name"] for c in manifest["provides"]["commands"]}
|
||||
assert "speckit.agent-context.update" in commands
|
||||
|
||||
def test_readme_exists(self):
|
||||
readme = EXT_DIR / "README.md"
|
||||
assert readme.is_file()
|
||||
text = readme.read_text(encoding="utf-8")
|
||||
assert "Coding Agent Context Extension" in text
|
||||
|
||||
def test_config_template_exists(self):
|
||||
cfg = EXT_DIR / "agent-context-config.yml"
|
||||
assert cfg.is_file()
|
||||
parsed = yaml.safe_load(cfg.read_text(encoding="utf-8"))
|
||||
assert "context_file" in parsed
|
||||
assert "context_markers" in parsed
|
||||
|
||||
def test_command_file_exists(self):
|
||||
cmd = EXT_DIR / "commands" / "speckit.agent-context.update.md"
|
||||
assert cmd.is_file()
|
||||
assert "agent-context-config.yml" in cmd.read_text(encoding="utf-8")
|
||||
|
||||
def test_bundled_scripts_exist(self):
|
||||
assert (EXT_DIR / "scripts" / "bash" / "update-agent-context.sh").is_file()
|
||||
assert (EXT_DIR / "scripts" / "powershell" / "update-agent-context.ps1").is_file()
|
||||
|
||||
def test_bash_script_reads_extension_config(self):
|
||||
text = (EXT_DIR / "scripts" / "bash" / "update-agent-context.sh").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
# The script must consult the extension config, not init-options.json
|
||||
assert "agent-context-config.yml" in text
|
||||
assert "context_file" in text
|
||||
assert "context_markers" in text
|
||||
|
||||
|
||||
# ── Catalog registration ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCatalogEntry:
|
||||
def test_catalog_lists_agent_context_as_bundled(self):
|
||||
catalog = json.loads(
|
||||
(PROJECT_ROOT / "extensions" / "catalog.json").read_text(encoding="utf-8")
|
||||
)
|
||||
entry = catalog["extensions"]["agent-context"]
|
||||
assert entry["bundled"] is True
|
||||
assert entry["id"] == "agent-context"
|
||||
assert entry["author"] == "spec-kit-core"
|
||||
|
||||
|
||||
# ── Marker resolution from extension config ──────────────────────────────────
|
||||
|
||||
|
||||
class _CtxIntegration(ClaudeIntegration):
|
||||
"""Use Claude as a concrete integration with a context_file."""
|
||||
|
||||
|
||||
class TestContextMarkerResolution:
|
||||
def test_defaults_when_ext_config_missing(self, tmp_path):
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == IntegrationBase.CONTEXT_MARKER_START
|
||||
assert end == IntegrationBase.CONTEXT_MARKER_END
|
||||
|
||||
def test_defaults_when_markers_field_missing(self, tmp_path):
|
||||
"""Config file exists with context_file but no context_markers key."""
|
||||
cfg_path = (
|
||||
tmp_path / ".specify" / "extensions" / "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cfg_path.write_text("context_file: CLAUDE.md\n", encoding="utf-8")
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == IntegrationBase.CONTEXT_MARKER_START
|
||||
assert end == IntegrationBase.CONTEXT_MARKER_END
|
||||
|
||||
def test_custom_markers_respected(self, tmp_path):
|
||||
_write_ext_config(
|
||||
tmp_path,
|
||||
context_markers={"start": "<!-- BEGIN -->", "end": "<!-- END -->"},
|
||||
)
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == "<!-- BEGIN -->"
|
||||
assert end == "<!-- END -->"
|
||||
|
||||
def test_partial_override_falls_back_for_missing_side(self, tmp_path):
|
||||
_write_ext_config(tmp_path, context_markers={"start": "<!-- ONLY START -->"})
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == "<!-- ONLY START -->"
|
||||
assert end == IntegrationBase.CONTEXT_MARKER_END
|
||||
|
||||
def test_invalid_markers_fall_back(self, tmp_path):
|
||||
_write_ext_config(tmp_path, context_markers={"start": 42, "end": ""})
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == IntegrationBase.CONTEXT_MARKER_START
|
||||
assert end == IntegrationBase.CONTEXT_MARKER_END
|
||||
|
||||
|
||||
# ── upsert_context_section / remove_context_section honor markers ───────────
|
||||
|
||||
|
||||
class TestUpsertWithCustomMarkers:
|
||||
def _setup(self, tmp_path: Path, markers: dict | None = None) -> _CtxIntegration:
|
||||
_write_ext_config(
|
||||
tmp_path,
|
||||
context_file="CLAUDE.md",
|
||||
**({"context_markers": markers} if markers is not None else {}),
|
||||
)
|
||||
return _CtxIntegration()
|
||||
|
||||
def test_upsert_uses_default_markers(self, tmp_path):
|
||||
i = self._setup(tmp_path)
|
||||
result = i.upsert_context_section(tmp_path)
|
||||
assert result is not None
|
||||
text = (tmp_path / "CLAUDE.md").read_text(encoding="utf-8")
|
||||
assert IntegrationBase.CONTEXT_MARKER_START in text
|
||||
assert IntegrationBase.CONTEXT_MARKER_END in text
|
||||
|
||||
def test_upsert_uses_custom_markers(self, tmp_path):
|
||||
i = self._setup(
|
||||
tmp_path, {"start": "<!-- BEGIN -->", "end": "<!-- END -->"}
|
||||
)
|
||||
i.upsert_context_section(tmp_path)
|
||||
text = (tmp_path / "CLAUDE.md").read_text(encoding="utf-8")
|
||||
assert "<!-- BEGIN -->" in text
|
||||
assert "<!-- END -->" in text
|
||||
# Defaults must not appear
|
||||
assert IntegrationBase.CONTEXT_MARKER_START not in text
|
||||
assert IntegrationBase.CONTEXT_MARKER_END not in text
|
||||
|
||||
def test_upsert_replaces_existing_custom_section(self, tmp_path):
|
||||
i = self._setup(
|
||||
tmp_path, {"start": "<!-- BEGIN -->", "end": "<!-- END -->"}
|
||||
)
|
||||
ctx = tmp_path / "CLAUDE.md"
|
||||
ctx.write_text(
|
||||
"# header\n\n<!-- BEGIN -->\nold body\n<!-- END -->\n\nfooter\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
i.upsert_context_section(tmp_path, plan_path="specs/001-foo/plan.md")
|
||||
text = ctx.read_text(encoding="utf-8")
|
||||
assert "old body" not in text
|
||||
assert "specs/001-foo/plan.md" in text
|
||||
assert text.startswith("# header\n")
|
||||
assert "footer" in text
|
||||
|
||||
def test_remove_uses_custom_markers(self, tmp_path):
|
||||
i = self._setup(
|
||||
tmp_path, {"start": "<!-- BEGIN -->", "end": "<!-- END -->"}
|
||||
)
|
||||
ctx = tmp_path / "CLAUDE.md"
|
||||
ctx.write_text(
|
||||
"preamble\n\n<!-- BEGIN -->\nbody\n<!-- END -->\nepilogue\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
removed = i.remove_context_section(tmp_path)
|
||||
assert removed is True
|
||||
remaining = ctx.read_text(encoding="utf-8")
|
||||
assert "<!-- BEGIN -->" not in remaining
|
||||
assert "<!-- END -->" not in remaining
|
||||
assert "body" not in remaining
|
||||
assert "preamble" in remaining
|
||||
assert "epilogue" in remaining
|
||||
|
||||
def test_remove_with_default_markers_unchanged_when_custom_in_file(self, tmp_path):
|
||||
# Extension config absent → default markers used. File contains only
|
||||
# custom markers — nothing should be removed.
|
||||
i = _CtxIntegration()
|
||||
ctx = tmp_path / "CLAUDE.md"
|
||||
original = "x\n<!-- BEGIN -->\nbody\n<!-- END -->\n"
|
||||
ctx.write_text(original, encoding="utf-8")
|
||||
assert i.remove_context_section(tmp_path) is False
|
||||
assert ctx.read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
# ── Extension disabled gates setup/teardown ──────────────────────────────────
|
||||
|
||||
|
||||
def _write_registry(project_root: Path, *, enabled: bool) -> None:
|
||||
registry = project_root / ".specify" / "extensions" / ".registry"
|
||||
registry.parent.mkdir(parents=True, exist_ok=True)
|
||||
registry.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"extensions": {
|
||||
"agent-context": {
|
||||
"version": "1.0.0",
|
||||
"enabled": enabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class TestExtensionEnabledGate:
|
||||
def test_enabled_helper_default_when_no_registry(self, tmp_path):
|
||||
assert IntegrationBase._agent_context_extension_enabled(tmp_path) is True
|
||||
|
||||
def test_enabled_helper_when_entry_present(self, tmp_path):
|
||||
_write_registry(tmp_path, enabled=True)
|
||||
assert IntegrationBase._agent_context_extension_enabled(tmp_path) is True
|
||||
|
||||
def test_disabled_helper_when_entry_disabled(self, tmp_path):
|
||||
_write_registry(tmp_path, enabled=False)
|
||||
assert IntegrationBase._agent_context_extension_enabled(tmp_path) is False
|
||||
|
||||
def test_upsert_skipped_when_disabled(self, tmp_path):
|
||||
_write_registry(tmp_path, enabled=False)
|
||||
i = _CtxIntegration()
|
||||
result = i.upsert_context_section(tmp_path)
|
||||
assert result is None
|
||||
assert not (tmp_path / "CLAUDE.md").exists()
|
||||
|
||||
def test_remove_skipped_when_disabled(self, tmp_path):
|
||||
_write_registry(tmp_path, enabled=False)
|
||||
i = _CtxIntegration()
|
||||
ctx = tmp_path / "CLAUDE.md"
|
||||
original = (
|
||||
f"head\n{IntegrationBase.CONTEXT_MARKER_START}\nbody\n"
|
||||
f"{IntegrationBase.CONTEXT_MARKER_END}\ntail\n"
|
||||
)
|
||||
ctx.write_text(original, encoding="utf-8")
|
||||
assert i.remove_context_section(tmp_path) is False
|
||||
# File must be unchanged when extension is disabled
|
||||
assert ctx.read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
# ── Extension config writers ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtensionConfigWriters:
|
||||
def test_clear_init_options_clears_ext_config_context_file(self, tmp_path):
|
||||
from specify_cli import _clear_init_options_for_integration
|
||||
|
||||
save_init_options(
|
||||
tmp_path,
|
||||
{"integration": "claude", "ai": "claude"},
|
||||
)
|
||||
_write_ext_config(tmp_path, context_file="CLAUDE.md")
|
||||
_clear_init_options_for_integration(tmp_path, "claude")
|
||||
cfg = _load_agent_context_config(tmp_path)
|
||||
assert cfg.get("context_file") == ""
|
||||
|
||||
def test_clear_init_options_creates_ext_config_when_missing(self, tmp_path):
|
||||
from specify_cli import _clear_init_options_for_integration
|
||||
|
||||
save_init_options(
|
||||
tmp_path,
|
||||
{"integration": "claude", "ai": "claude"},
|
||||
)
|
||||
_clear_init_options_for_integration(tmp_path, "claude")
|
||||
cfg = _load_agent_context_config(tmp_path)
|
||||
assert cfg.get("context_file") == ""
|
||||
|
||||
def test_clear_init_options_removes_legacy_context_keys_even_when_not_active(
|
||||
self, tmp_path
|
||||
):
|
||||
from specify_cli import _clear_init_options_for_integration
|
||||
|
||||
save_init_options(
|
||||
tmp_path,
|
||||
{
|
||||
"integration": "copilot",
|
||||
"ai": "copilot",
|
||||
"context_file": "CLAUDE.md",
|
||||
"context_markers": {"start": "<!-- X -->", "end": "<!-- Y -->"},
|
||||
},
|
||||
)
|
||||
_clear_init_options_for_integration(tmp_path, "claude")
|
||||
opts = load_init_options(tmp_path)
|
||||
assert opts["integration"] == "copilot"
|
||||
assert opts["ai"] == "copilot"
|
||||
assert "context_file" not in opts
|
||||
assert "context_markers" not in opts
|
||||
|
||||
def test_update_init_options_writes_context_file_to_ext_config(self, tmp_path):
|
||||
from specify_cli import _update_init_options_for_integration
|
||||
|
||||
# Pre-create the extension config so _update_init_options_for_integration
|
||||
# updates it (rather than skipping it when ext config doesn't exist yet).
|
||||
_write_ext_config(tmp_path, context_file="")
|
||||
i = _CtxIntegration()
|
||||
_update_init_options_for_integration(tmp_path, i, script_type="sh")
|
||||
# init-options.json must NOT have context_file or context_markers
|
||||
opts = load_init_options(tmp_path)
|
||||
assert "context_file" not in opts
|
||||
assert "context_markers" not in opts
|
||||
# Extension config must have them
|
||||
cfg = _load_agent_context_config(tmp_path)
|
||||
assert cfg["context_file"] == i.context_file
|
||||
assert "context_markers" in cfg
|
||||
|
||||
def test_update_init_options_preserves_custom_markers(self, tmp_path):
|
||||
from specify_cli import _update_init_options_for_integration
|
||||
|
||||
_write_ext_config(
|
||||
tmp_path,
|
||||
context_file="",
|
||||
context_markers={"start": "<!-- B -->", "end": "<!-- E -->"},
|
||||
)
|
||||
i = _CtxIntegration()
|
||||
_update_init_options_for_integration(tmp_path, i)
|
||||
cfg = _load_agent_context_config(tmp_path)
|
||||
assert cfg["context_markers"] == {"start": "<!-- B -->", "end": "<!-- E -->"}
|
||||
|
||||
def test_reinit_preserves_custom_markers(self, tmp_path):
|
||||
"""specify init (reinit) must not overwrite user-customised markers."""
|
||||
from specify_cli import _update_agent_context_config_file
|
||||
|
||||
# Simulate existing project with custom markers
|
||||
_write_ext_config(
|
||||
tmp_path,
|
||||
context_file="CLAUDE.md",
|
||||
context_markers={"start": "<!-- CUSTOM -->", "end": "<!-- /CUSTOM -->"},
|
||||
)
|
||||
# Re-running init updates context_file but must preserve markers
|
||||
_update_agent_context_config_file(
|
||||
tmp_path, "CLAUDE.md", preserve_markers=True
|
||||
)
|
||||
cfg = _load_agent_context_config(tmp_path)
|
||||
assert cfg["context_markers"] == {
|
||||
"start": "<!-- CUSTOM -->",
|
||||
"end": "<!-- /CUSTOM -->",
|
||||
}
|
||||
|
||||
|
||||
# ── Deprecation warning on upsert ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDeprecationWarning:
|
||||
def test_upsert_emits_deprecation_warning(self, tmp_path, capsys):
|
||||
"""upsert_context_section must emit a deprecation notice on stdout."""
|
||||
from tests.conftest import strip_ansi
|
||||
|
||||
i = _CtxIntegration()
|
||||
_write_ext_config(tmp_path, context_file="CLAUDE.md")
|
||||
i.upsert_context_section(tmp_path)
|
||||
captured = capsys.readouterr()
|
||||
plain = strip_ansi(captured.out)
|
||||
assert "Deprecation" in plain
|
||||
assert "v0.12.0" in plain
|
||||
assert "agent-context" in plain
|
||||
|
||||
def test_upsert_no_warning_when_disabled(self, tmp_path, capsys):
|
||||
"""No deprecation warning when agent-context extension is disabled."""
|
||||
_write_registry(tmp_path, enabled=False)
|
||||
i = _CtxIntegration()
|
||||
i.upsert_context_section(tmp_path)
|
||||
captured = capsys.readouterr()
|
||||
assert "Deprecation" not in captured.out
|
||||
|
||||
|
||||
# ── Corrupt / invalid extension config ───────────────────────────────────────
|
||||
|
||||
|
||||
class TestCorruptExtensionConfig:
|
||||
def test_marker_resolution_with_corrupt_yaml(self, tmp_path):
|
||||
"""Corrupt YAML in agent-context-config.yml falls back to defaults."""
|
||||
cfg_path = (
|
||||
tmp_path / ".specify" / "extensions" / "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cfg_path.write_text(": invalid: yaml: {{{\n", encoding="utf-8")
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == IntegrationBase.CONTEXT_MARKER_START
|
||||
assert end == IntegrationBase.CONTEXT_MARKER_END
|
||||
|
||||
def test_upsert_with_corrupt_config_uses_defaults(self, tmp_path):
|
||||
"""upsert_context_section still works when config YAML is corrupt."""
|
||||
cfg_path = (
|
||||
tmp_path / ".specify" / "extensions" / "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cfg_path.write_text("not valid yaml: {{{\n", encoding="utf-8")
|
||||
i = _CtxIntegration()
|
||||
result = i.upsert_context_section(tmp_path)
|
||||
assert result is not None
|
||||
text = (tmp_path / "CLAUDE.md").read_text(encoding="utf-8")
|
||||
assert IntegrationBase.CONTEXT_MARKER_START in text
|
||||
assert IntegrationBase.CONTEXT_MARKER_END in text
|
||||
|
||||
def test_marker_resolution_with_non_dict_yaml(self, tmp_path):
|
||||
"""Config file containing a scalar (not a dict) falls back to defaults."""
|
||||
cfg_path = (
|
||||
tmp_path / ".specify" / "extensions" / "agent-context"
|
||||
/ "agent-context-config.yml"
|
||||
)
|
||||
cfg_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cfg_path.write_text("just a string\n", encoding="utf-8")
|
||||
i = _CtxIntegration()
|
||||
start, end = i._resolve_context_markers(tmp_path)
|
||||
assert start == IntegrationBase.CONTEXT_MARKER_START
|
||||
assert end == IntegrationBase.CONTEXT_MARKER_END
|
||||
@@ -87,14 +87,7 @@ class TestInitIntegrationFlag:
|
||||
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text(encoding="utf-8"))
|
||||
assert opts["integration"] == "copilot"
|
||||
# context_file lives in the agent-context extension config, not init-options.json
|
||||
assert "context_file" not in opts
|
||||
|
||||
import yaml as _yaml
|
||||
ext_cfg_path = project / ".specify" / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
assert ext_cfg_path.exists(), "agent-context extension config must be created on init"
|
||||
ext_cfg = _yaml.safe_load(ext_cfg_path.read_text(encoding="utf-8"))
|
||||
assert ext_cfg["context_file"] == ".github/copilot-instructions.md"
|
||||
assert opts["context_file"] == ".github/copilot-instructions.md"
|
||||
|
||||
assert (project / ".specify" / "integrations" / "copilot.manifest.json").exists()
|
||||
|
||||
|
||||
@@ -226,8 +226,8 @@ class MarkdownIntegrationTests:
|
||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
||||
|
||||
def test_init_options_includes_context_file(self, tmp_path):
|
||||
"""agent-context extension config must include context_file for the active integration."""
|
||||
import yaml
|
||||
"""init-options.json must include context_file for the active integration."""
|
||||
import json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -243,17 +243,15 @@ class MarkdownIntegrationTests:
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
ext_cfg_path = project / ".specify" / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
ext_cfg = yaml.safe_load(ext_cfg_path.read_text(encoding="utf-8")) if ext_cfg_path.exists() else {}
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text())
|
||||
i = get_integration(self.KEY)
|
||||
assert ext_cfg.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {ext_cfg.get('context_file')!r}"
|
||||
assert opts.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
|
||||
)
|
||||
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"agent-context.update",
|
||||
"analyze", "checklist", "clarify", "constitution",
|
||||
"implement", "plan", "specify", "tasks", "taskstoissues",
|
||||
]
|
||||
@@ -293,16 +291,6 @@ class MarkdownIntegrationTests:
|
||||
files.append(".specify/workflows/speckit/workflow.yml")
|
||||
files.append(".specify/workflows/workflow-registry.json")
|
||||
|
||||
# Bundled agent-context extension
|
||||
files.append(".specify/extensions.yml")
|
||||
files.append(".specify/extensions/.registry")
|
||||
files.append(".specify/extensions/agent-context/README.md")
|
||||
files.append(".specify/extensions/agent-context/agent-context-config.yml")
|
||||
files.append(".specify/extensions/agent-context/commands/speckit.agent-context.update.md")
|
||||
files.append(".specify/extensions/agent-context/extension.yml")
|
||||
files.append(".specify/extensions/agent-context/scripts/bash/update-agent-context.sh")
|
||||
files.append(".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1")
|
||||
|
||||
# Agent context file (if set)
|
||||
if i.context_file:
|
||||
files.append(i.context_file)
|
||||
|
||||
@@ -357,8 +357,8 @@ class SkillsIntegrationTests:
|
||||
assert skills_dir.is_dir(), f"Skills directory {skills_dir} not created"
|
||||
|
||||
def test_init_options_includes_context_file(self, tmp_path):
|
||||
"""agent-context extension config must include context_file for the active integration."""
|
||||
import yaml
|
||||
"""init-options.json must include context_file for the active integration."""
|
||||
import json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -374,11 +374,10 @@ class SkillsIntegrationTests:
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
ext_cfg_path = project / ".specify" / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
ext_cfg = yaml.safe_load(ext_cfg_path.read_text(encoding="utf-8")) if ext_cfg_path.exists() else {}
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text())
|
||||
i = get_integration(self.KEY)
|
||||
assert ext_cfg.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {ext_cfg.get('context_file')!r}"
|
||||
assert opts.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
|
||||
)
|
||||
|
||||
# -- IntegrationOption ------------------------------------------------
|
||||
@@ -403,11 +402,9 @@ class SkillsIntegrationTests:
|
||||
skills_prefix = i.config["folder"].rstrip("/") + "/" + i.config.get("commands_subdir", "skills")
|
||||
|
||||
files = []
|
||||
# Skill files (core commands)
|
||||
# Skill files
|
||||
for cmd in self._SKILL_COMMANDS:
|
||||
files.append(f"{skills_prefix}/speckit-{cmd}/SKILL.md")
|
||||
# Extension-installed skill (agent-context)
|
||||
files.append(f"{skills_prefix}/speckit-agent-context-update/SKILL.md")
|
||||
# Integration metadata
|
||||
files += [
|
||||
".specify/init-options.json",
|
||||
@@ -446,15 +443,6 @@ class SkillsIntegrationTests:
|
||||
".specify/workflows/speckit/workflow.yml",
|
||||
".specify/workflows/workflow-registry.json",
|
||||
]
|
||||
# Bundled agent-context extension
|
||||
files.append(".specify/extensions.yml")
|
||||
files.append(".specify/extensions/.registry")
|
||||
files.append(".specify/extensions/agent-context/README.md")
|
||||
files.append(".specify/extensions/agent-context/agent-context-config.yml")
|
||||
files.append(".specify/extensions/agent-context/commands/speckit.agent-context.update.md")
|
||||
files.append(".specify/extensions/agent-context/extension.yml")
|
||||
files.append(".specify/extensions/agent-context/scripts/bash/update-agent-context.sh")
|
||||
files.append(".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1")
|
||||
# Agent context file (if set)
|
||||
if i.context_file:
|
||||
files.append(i.context_file)
|
||||
|
||||
@@ -457,8 +457,8 @@ class TomlIntegrationTests:
|
||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
||||
|
||||
def test_init_options_includes_context_file(self, tmp_path):
|
||||
"""agent-context extension config must include context_file for the active integration."""
|
||||
import yaml
|
||||
"""init-options.json must include context_file for the active integration."""
|
||||
import json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -474,17 +474,15 @@ class TomlIntegrationTests:
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
ext_cfg_path = project / ".specify" / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
ext_cfg = yaml.safe_load(ext_cfg_path.read_text(encoding="utf-8")) if ext_cfg_path.exists() else {}
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text())
|
||||
i = get_integration(self.KEY)
|
||||
assert ext_cfg.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {ext_cfg.get('context_file')!r}"
|
||||
assert opts.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
|
||||
)
|
||||
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"agent-context.update",
|
||||
"analyze",
|
||||
"checklist",
|
||||
"clarify",
|
||||
@@ -545,16 +543,6 @@ class TomlIntegrationTests:
|
||||
files.append(".specify/workflows/speckit/workflow.yml")
|
||||
files.append(".specify/workflows/workflow-registry.json")
|
||||
|
||||
# Bundled agent-context extension
|
||||
files.append(".specify/extensions.yml")
|
||||
files.append(".specify/extensions/.registry")
|
||||
files.append(".specify/extensions/agent-context/README.md")
|
||||
files.append(".specify/extensions/agent-context/agent-context-config.yml")
|
||||
files.append(".specify/extensions/agent-context/commands/speckit.agent-context.update.md")
|
||||
files.append(".specify/extensions/agent-context/extension.yml")
|
||||
files.append(".specify/extensions/agent-context/scripts/bash/update-agent-context.sh")
|
||||
files.append(".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1")
|
||||
|
||||
# Agent context file (if set)
|
||||
if i.context_file:
|
||||
files.append(i.context_file)
|
||||
|
||||
@@ -336,8 +336,8 @@ class YamlIntegrationTests:
|
||||
assert len(commands) > 0, f"No command files in {cmd_dir}"
|
||||
|
||||
def test_init_options_includes_context_file(self, tmp_path):
|
||||
"""agent-context extension config must include context_file for the active integration."""
|
||||
import yaml
|
||||
"""init-options.json must include context_file for the active integration."""
|
||||
import json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -353,17 +353,15 @@ class YamlIntegrationTests:
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
ext_cfg_path = project / ".specify" / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
ext_cfg = yaml.safe_load(ext_cfg_path.read_text(encoding="utf-8")) if ext_cfg_path.exists() else {}
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text())
|
||||
i = get_integration(self.KEY)
|
||||
assert ext_cfg.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {ext_cfg.get('context_file')!r}"
|
||||
assert opts.get("context_file") == i.context_file, (
|
||||
f"Expected context_file={i.context_file!r}, got {opts.get('context_file')!r}"
|
||||
)
|
||||
|
||||
# -- Complete file inventory ------------------------------------------
|
||||
|
||||
COMMAND_STEMS = [
|
||||
"agent-context.update",
|
||||
"analyze",
|
||||
"checklist",
|
||||
"clarify",
|
||||
@@ -424,16 +422,6 @@ class YamlIntegrationTests:
|
||||
files.append(".specify/workflows/speckit/workflow.yml")
|
||||
files.append(".specify/workflows/workflow-registry.json")
|
||||
|
||||
# Bundled agent-context extension
|
||||
files.append(".specify/extensions.yml")
|
||||
files.append(".specify/extensions/.registry")
|
||||
files.append(".specify/extensions/agent-context/README.md")
|
||||
files.append(".specify/extensions/agent-context/agent-context-config.yml")
|
||||
files.append(".specify/extensions/agent-context/commands/speckit.agent-context.update.md")
|
||||
files.append(".specify/extensions/agent-context/extension.yml")
|
||||
files.append(".specify/extensions/agent-context/scripts/bash/update-agent-context.sh")
|
||||
files.append(".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1")
|
||||
|
||||
# Agent context file (if set)
|
||||
if i.context_file:
|
||||
files.append(i.context_file)
|
||||
|
||||
@@ -178,7 +178,6 @@ class TestCopilotIntegration:
|
||||
assert result.exit_code == 0
|
||||
actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file())
|
||||
expected = sorted([
|
||||
".github/agents/speckit.agent-context.update.agent.md",
|
||||
".github/agents/speckit.analyze.agent.md",
|
||||
".github/agents/speckit.checklist.agent.md",
|
||||
".github/agents/speckit.clarify.agent.md",
|
||||
@@ -188,7 +187,6 @@ class TestCopilotIntegration:
|
||||
".github/agents/speckit.specify.agent.md",
|
||||
".github/agents/speckit.tasks.agent.md",
|
||||
".github/agents/speckit.taskstoissues.agent.md",
|
||||
".github/prompts/speckit.agent-context.update.prompt.md",
|
||||
".github/prompts/speckit.analyze.prompt.md",
|
||||
".github/prompts/speckit.checklist.prompt.md",
|
||||
".github/prompts/speckit.clarify.prompt.md",
|
||||
@@ -200,14 +198,6 @@ class TestCopilotIntegration:
|
||||
".github/prompts/speckit.taskstoissues.prompt.md",
|
||||
".vscode/settings.json",
|
||||
".github/copilot-instructions.md",
|
||||
".specify/extensions.yml",
|
||||
".specify/extensions/.registry",
|
||||
".specify/extensions/agent-context/README.md",
|
||||
".specify/extensions/agent-context/agent-context-config.yml",
|
||||
".specify/extensions/agent-context/commands/speckit.agent-context.update.md",
|
||||
".specify/extensions/agent-context/extension.yml",
|
||||
".specify/extensions/agent-context/scripts/bash/update-agent-context.sh",
|
||||
".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1",
|
||||
".specify/integration.json",
|
||||
".specify/init-options.json",
|
||||
".specify/integrations/copilot.manifest.json",
|
||||
@@ -248,7 +238,6 @@ class TestCopilotIntegration:
|
||||
assert result.exit_code == 0
|
||||
actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file())
|
||||
expected = sorted([
|
||||
".github/agents/speckit.agent-context.update.agent.md",
|
||||
".github/agents/speckit.analyze.agent.md",
|
||||
".github/agents/speckit.checklist.agent.md",
|
||||
".github/agents/speckit.clarify.agent.md",
|
||||
@@ -258,7 +247,6 @@ class TestCopilotIntegration:
|
||||
".github/agents/speckit.specify.agent.md",
|
||||
".github/agents/speckit.tasks.agent.md",
|
||||
".github/agents/speckit.taskstoissues.agent.md",
|
||||
".github/prompts/speckit.agent-context.update.prompt.md",
|
||||
".github/prompts/speckit.analyze.prompt.md",
|
||||
".github/prompts/speckit.checklist.prompt.md",
|
||||
".github/prompts/speckit.clarify.prompt.md",
|
||||
@@ -270,14 +258,6 @@ class TestCopilotIntegration:
|
||||
".github/prompts/speckit.taskstoissues.prompt.md",
|
||||
".vscode/settings.json",
|
||||
".github/copilot-instructions.md",
|
||||
".specify/extensions.yml",
|
||||
".specify/extensions/.registry",
|
||||
".specify/extensions/agent-context/README.md",
|
||||
".specify/extensions/agent-context/agent-context-config.yml",
|
||||
".specify/extensions/agent-context/commands/speckit.agent-context.update.md",
|
||||
".specify/extensions/agent-context/extension.yml",
|
||||
".specify/extensions/agent-context/scripts/bash/update-agent-context.sh",
|
||||
".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1",
|
||||
".specify/integration.json",
|
||||
".specify/init-options.json",
|
||||
".specify/integrations/copilot.manifest.json",
|
||||
@@ -644,20 +624,10 @@ class TestCopilotSkillsMode:
|
||||
assert result.exit_code == 0, f"init failed: {result.output}"
|
||||
actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file())
|
||||
expected = sorted([
|
||||
# Skill files (core + extension-installed agent-context command)
|
||||
# Skill files
|
||||
*[f".github/skills/speckit-{cmd}/SKILL.md" for cmd in self._SKILL_COMMANDS],
|
||||
".github/skills/speckit-agent-context-update/SKILL.md",
|
||||
# Context file
|
||||
".github/copilot-instructions.md",
|
||||
# Bundled agent-context extension
|
||||
".specify/extensions.yml",
|
||||
".specify/extensions/.registry",
|
||||
".specify/extensions/agent-context/README.md",
|
||||
".specify/extensions/agent-context/agent-context-config.yml",
|
||||
".specify/extensions/agent-context/commands/speckit.agent-context.update.md",
|
||||
".specify/extensions/agent-context/extension.yml",
|
||||
".specify/extensions/agent-context/scripts/bash/update-agent-context.sh",
|
||||
".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1",
|
||||
# Integration metadata
|
||||
".specify/init-options.json",
|
||||
".specify/integration.json",
|
||||
|
||||
@@ -195,39 +195,6 @@ class TestGenericIntegration:
|
||||
content = implement_file.read_text(encoding="utf-8")
|
||||
assert ".specify/memory/constitution.md" in content
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"command_stem",
|
||||
[
|
||||
"analyze",
|
||||
"checklist",
|
||||
"clarify",
|
||||
"implement",
|
||||
"plan",
|
||||
"specify",
|
||||
"tasks",
|
||||
"taskstoissues",
|
||||
],
|
||||
)
|
||||
def test_command_loads_constitution_context(self, tmp_path, command_stem):
|
||||
"""Every command except constitution must reference constitution.md."""
|
||||
i = get_integration("generic")
|
||||
m = IntegrationManifest("generic", tmp_path)
|
||||
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
||||
cmd_file = tmp_path / ".custom" / "cmds" / f"speckit.{command_stem}.md"
|
||||
assert cmd_file.exists(), f"Command file missing: {cmd_file.name}"
|
||||
content = cmd_file.read_text(encoding="utf-8")
|
||||
assert "constitution.md" in content, (
|
||||
f"speckit.{command_stem}.md must reference constitution.md"
|
||||
)
|
||||
|
||||
def test_constitution_command_exists(self, tmp_path):
|
||||
"""The constitution command itself must exist but is not required to load itself."""
|
||||
i = get_integration("generic")
|
||||
m = IntegrationManifest("generic", tmp_path)
|
||||
i.setup(tmp_path, m, parsed_options={"commands_dir": ".custom/cmds"})
|
||||
cmd_file = tmp_path / ".custom" / "cmds" / "speckit.constitution.md"
|
||||
assert cmd_file.exists()
|
||||
|
||||
# -- CLI --------------------------------------------------------------
|
||||
|
||||
def test_cli_generic_without_commands_dir_fails(self, tmp_path):
|
||||
@@ -244,8 +211,8 @@ class TestGenericIntegration:
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_init_options_includes_context_file(self, tmp_path):
|
||||
"""agent-context extension config must include context_file for the generic integration."""
|
||||
import yaml
|
||||
"""init-options.json must include context_file for the generic integration."""
|
||||
import json
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -262,9 +229,8 @@ class TestGenericIntegration:
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
assert result.exit_code == 0
|
||||
ext_cfg_path = project / ".specify" / "extensions" / "agent-context" / "agent-context-config.yml"
|
||||
ext_cfg = yaml.safe_load(ext_cfg_path.read_text(encoding="utf-8")) if ext_cfg_path.exists() else {}
|
||||
assert ext_cfg.get("context_file") == "AGENTS.md"
|
||||
opts = json.loads((project / ".specify" / "init-options.json").read_text())
|
||||
assert opts.get("context_file") == "AGENTS.md"
|
||||
|
||||
def test_complete_file_inventory_sh(self, tmp_path):
|
||||
"""Every file produced by specify init --integration generic --ai-commands-dir ... --script sh."""
|
||||
@@ -299,14 +265,6 @@ class TestGenericIntegration:
|
||||
".myagent/commands/speckit.specify.md",
|
||||
".myagent/commands/speckit.tasks.md",
|
||||
".myagent/commands/speckit.taskstoissues.md",
|
||||
".specify/extensions.yml",
|
||||
".specify/extensions/.registry",
|
||||
".specify/extensions/agent-context/README.md",
|
||||
".specify/extensions/agent-context/agent-context-config.yml",
|
||||
".specify/extensions/agent-context/commands/speckit.agent-context.update.md",
|
||||
".specify/extensions/agent-context/extension.yml",
|
||||
".specify/extensions/agent-context/scripts/bash/update-agent-context.sh",
|
||||
".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1",
|
||||
".specify/init-options.json",
|
||||
".specify/integration.json",
|
||||
".specify/integrations/generic.manifest.json",
|
||||
@@ -363,14 +321,6 @@ class TestGenericIntegration:
|
||||
".myagent/commands/speckit.specify.md",
|
||||
".myagent/commands/speckit.tasks.md",
|
||||
".myagent/commands/speckit.taskstoissues.md",
|
||||
".specify/extensions.yml",
|
||||
".specify/extensions/.registry",
|
||||
".specify/extensions/agent-context/README.md",
|
||||
".specify/extensions/agent-context/agent-context-config.yml",
|
||||
".specify/extensions/agent-context/commands/speckit.agent-context.update.md",
|
||||
".specify/extensions/agent-context/extension.yml",
|
||||
".specify/extensions/agent-context/scripts/bash/update-agent-context.sh",
|
||||
".specify/extensions/agent-context/scripts/powershell/update-agent-context.ps1",
|
||||
".specify/init-options.json",
|
||||
".specify/integration.json",
|
||||
".specify/integrations/generic.manifest.json",
|
||||
|
||||
@@ -241,15 +241,10 @@ class TestHermesIntegration(SkillsIntegrationTests):
|
||||
p.relative_to(project).as_posix()
|
||||
for p in project.rglob("*") if p.is_file()
|
||||
)
|
||||
# Ensure no core .hermes/skills/speckit-*/SKILL.md in project dir
|
||||
# (extension-installed skills like agent-context-update may appear)
|
||||
hermes_skill_files = [
|
||||
f for f in actual
|
||||
if f.startswith(".hermes/skills/speckit-")
|
||||
and "agent-context" not in f
|
||||
]
|
||||
# Ensure no .hermes/skills/speckit-*/SKILL.md in project dir
|
||||
hermes_skill_files = [f for f in actual if f.startswith(".hermes/skills/speckit-")]
|
||||
assert hermes_skill_files == [], (
|
||||
f"Expected no local core SKILL.md files, found: {hermes_skill_files}"
|
||||
f"Expected no local SKILL.md files, found: {hermes_skill_files}"
|
||||
)
|
||||
# Ensure the marker exists (empty dir won't appear in file listing)
|
||||
assert (project / ".hermes" / "skills").is_dir()
|
||||
@@ -279,15 +274,9 @@ class TestHermesIntegration(SkillsIntegrationTests):
|
||||
p.relative_to(project).as_posix()
|
||||
for p in project.rglob("*") if p.is_file()
|
||||
)
|
||||
# Ensure no core .hermes/skills/speckit-*/SKILL.md in project dir
|
||||
# (extension-installed skills like agent-context-update may appear)
|
||||
hermes_skill_files = [
|
||||
f for f in actual
|
||||
if f.startswith(".hermes/skills/speckit-")
|
||||
and "agent-context" not in f
|
||||
]
|
||||
hermes_skill_files = [f for f in actual if f.startswith(".hermes/skills/speckit-")]
|
||||
assert hermes_skill_files == [], (
|
||||
f"Expected no local core SKILL.md files, found: {hermes_skill_files}"
|
||||
f"Expected no local SKILL.md files, found: {hermes_skill_files}"
|
||||
)
|
||||
assert (project / ".hermes" / "skills").is_dir()
|
||||
|
||||
@@ -353,10 +342,6 @@ class TestHermesAutoPromote:
|
||||
assert (home / ".hermes" / "skills" / "speckit-plan" / "SKILL.md").exists()
|
||||
# Local marker should exist
|
||||
assert (target / ".hermes" / "skills").is_dir()
|
||||
# No core SKILL.md files in project-local dir
|
||||
# (extension-installed skills like agent-context-update may appear)
|
||||
local_skills = [
|
||||
d for d in (target / ".hermes" / "skills").iterdir()
|
||||
if "agent-context" not in d.name
|
||||
]
|
||||
# No SKILL.md files in project-local dir
|
||||
local_skills = list((target / ".hermes" / "skills").iterdir())
|
||||
assert local_skills == [], f"Local skills dir should be empty, got: {local_skills}"
|
||||
|
||||
@@ -255,7 +255,7 @@ class TestIntegrationInstall:
|
||||
assert updated["speckit_version"] == "0.8.11"
|
||||
assert updated["integration"] == "claude"
|
||||
assert updated["ai"] == "claude"
|
||||
assert "context_file" not in updated
|
||||
assert updated["context_file"] == "CLAUDE.md"
|
||||
|
||||
def test_install_additional_preserves_shared_manifest(self, tmp_path):
|
||||
project = _init_project(tmp_path, "claude")
|
||||
@@ -1250,7 +1250,7 @@ class TestIntegrationUpgrade:
|
||||
assert updated["speckit_version"] == "0.8.11"
|
||||
assert updated["integration"] == "gemini"
|
||||
assert updated["ai"] == "gemini"
|
||||
assert "context_file" not in updated
|
||||
assert updated["context_file"] == "GEMINI.md"
|
||||
|
||||
def test_upgrade_does_not_persist_state_when_shared_infra_refresh_fails(self, tmp_path, monkeypatch):
|
||||
project = _init_project(tmp_path, "claude")
|
||||
@@ -1376,16 +1376,11 @@ class TestIntegrationUpgrade:
|
||||
new_commands = sorted(canonical.glob("speckit.*.md"))
|
||||
assert len(new_commands) > 0, "Commands should exist in .opencode/commands/"
|
||||
|
||||
# Stale files removed from legacy dir (extension-installed commands
|
||||
# like agent-context.update may still appear — only check the original
|
||||
# core command stems that should have been migrated).
|
||||
core_remaining = [
|
||||
f for f in legacy.glob("speckit.*.md")
|
||||
if "agent-context" not in f.name
|
||||
]
|
||||
assert len(core_remaining) == 0, (
|
||||
f"Legacy .opencode/command/ should have no core speckit files after upgrade, "
|
||||
f"found: {[f.name for f in core_remaining]}"
|
||||
# Stale files removed from legacy dir
|
||||
remaining = list(legacy.glob("speckit.*.md"))
|
||||
assert len(remaining) == 0, (
|
||||
f"Legacy .opencode/command/ should have no speckit files after upgrade, "
|
||||
f"found: {[f.name for f in remaining]}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3807,67 +3807,6 @@ class TestExtensionAddCLI:
|
||||
assert "bundled with spec-kit" in result.output
|
||||
assert "reinstall" in result.output.lower()
|
||||
|
||||
def test_add_from_url_prompts_before_spinner(self, tmp_path):
|
||||
"""Confirm prompt for --from <url> must fire before the console.status spinner.
|
||||
|
||||
Regression test for #2783: typer.confirm() inside console.status()
|
||||
was overwritten by the Rich spinner, making the command appear hung.
|
||||
"""
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch, MagicMock
|
||||
from specify_cli import app
|
||||
|
||||
project_dir = tmp_path / "test-project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / ".specify").mkdir()
|
||||
|
||||
call_order: list[str] = []
|
||||
|
||||
original_status = MagicMock()
|
||||
|
||||
def record_status(*args, **kwargs):
|
||||
call_order.append("spinner")
|
||||
return original_status
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.object(Path, "cwd", return_value=project_dir), \
|
||||
patch("specify_cli.console.status", side_effect=record_status), \
|
||||
patch("typer.confirm", side_effect=lambda *a, **kw: (call_order.append("confirm"), False)[-1]):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["extension", "add", "my-ext", "--from", "https://example.com/ext.zip"],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
assert "confirm" in call_order, "confirm prompt was never called"
|
||||
# The confirm must fire BEFORE the spinner is entered
|
||||
if "spinner" in call_order:
|
||||
assert call_order.index("confirm") < call_order.index("spinner"), \
|
||||
f"confirm must precede spinner, got: {call_order}"
|
||||
assert result.exit_code == 0 # user declined → clean exit
|
||||
|
||||
def test_add_from_url_cancel_exits_cleanly(self, tmp_path):
|
||||
"""Declining the --from <url> confirmation should exit with code 0."""
|
||||
from typer.testing import CliRunner
|
||||
from unittest.mock import patch
|
||||
from specify_cli import app
|
||||
|
||||
project_dir = tmp_path / "test-project"
|
||||
project_dir.mkdir()
|
||||
(project_dir / ".specify").mkdir()
|
||||
|
||||
runner = CliRunner()
|
||||
with patch.object(Path, "cwd", return_value=project_dir), \
|
||||
patch("typer.confirm", return_value=False):
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["extension", "add", "my-ext", "--from", "https://example.com/ext.zip"],
|
||||
catch_exceptions=True,
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "Cancelled" in result.output
|
||||
|
||||
|
||||
class TestDownloadExtensionBundled:
|
||||
"""Tests for download_extension handling of bundled extensions."""
|
||||
|
||||
Reference in New Issue
Block a user