Compare commits

..

91 Commits

Author SHA1 Message Date
github-actions[bot]
90024d7883 chore: bump version to 0.8.6 2026-05-06 11:58:36 +00:00
Copilot
b4060d5620 Load constitution context in /speckit.implement to enforce governance during implementation (#2460)
* Initial plan

* fix implement command to load constitution context

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/05663d9d-149b-4c13-a22d-2552b3fa619c

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-06 06:56:59 -05:00
Manfred Riem
0f26551814 feat: improve catalog submission templates and CODEOWNERS (#2401)
Simplify the community catalog submission flow to use issue templates
with manual maintainer review (no automation scripts or workflows).

- Add explicit CODEOWNERS entries for catalog.community.json files so
  submissions are automatically assigned to a maintainer for review
- Improve preset submission template:
  - Add 'Required Extensions' optional field
  - Make 'Templates Provided' optional (supports command-only presets)
  - Add 'Number of Scripts' optional field

The existing extension and preset issue templates already collect all
required catalog metadata. Maintainers review submissions and manually
update the catalog JSON files.

Closes #2400
2026-05-05 16:59:25 -05:00
Ayesha Aziz
30e6fa9e32 fix: validate URL scheme in build_github_request (#2449)
* fix: validate URL scheme in build_github_request

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* test: add missing hostname validation test for build_github_request

* fix: update docstring and fix import grouping per Copilot feedback

* fix: sort imports and simplify url validation in build_github_request

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 13:28:29 -05:00
Dyan Galih
10f63c914d Add Architecture Guard to community catalog (#2430)
* feat: add Architecture Guard to community catalog

- Add architecture-guard v1.4.0 extension entry to catalog
- Add entry to README community extensions table
- Includes built-in Laravel-specific governance rules

* chore: update catalog timestamp to 2026-05-05

* fix: address PR feedback

- Add 'governance' category to README legend (used by Architecture Guard)
- Update architecture-guard timestamps to 2026-05-05 (submission date)
- Align with published extension behavior (Laravel support now built-in)

* chore: update Architecture Guard category to process

- Changed from 'governance' to 'process' (official category)
- Aligns with schema in EXTENSION-PUBLISHING-GUIDE.md
- Removed 'governance' from category legend (not an official category)

* chore: update timestamps to actual UTC datetime

- Top-level updated_at: 2026-05-05T07:26:00Z
- Entry created_at/updated_at: 2026-05-05T07:26:00Z
- Replaces placeholder 00:00:00Z with actual submission time
2026-05-05 10:48:19 -05:00
낮해달밤
0d8685aa80 Add multi-model-review extension to community catalog (#2446)
Co-authored-by: formin <formin@sds.co.kr>
2026-05-04 17:14:31 -05:00
Ben Lawson
4a8f19cc63 Update Ralph Loop to v1.0.2 (#2435) 2026-05-04 15:50:12 -05:00
Pascal THUET
09f7657f5b Pin GitHub Actions by SHA (#2441) 2026-05-04 14:08:07 -05:00
Pascal THUET
a7201c183e fix(workflows): require project for catalog list (#2436) 2026-05-04 12:00:28 -05:00
Thorsten Hindermann
1994bd766e Add agent-parity-governance to community catalog (#2382) 2026-05-04 11:43:42 -05:00
Manfred Riem
f47c2eb468 chore: release 0.8.5, begin 0.8.6.dev0 development (#2447)
* chore: bump version to 0.8.5

* chore: begin 0.8.6.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-04 11:39:08 -05:00
Alex Vieira
05d9aa3e90 feat(presets): add Spec2Cloud preset for Azure deployment workflow (#2413)
* feat(presets): add Spec2Cloud preset for Azure deployment workflow

Co-authored-by: Copilot <copilot@github.com>

* feat(presets): add Spec2Cloud preset details to community catalog

* fix(presets): update Spec2Cloud URL to point to the correct GitHub repository

* feat(presets): update Spec2Cloud entry with created_at and updated_at timestamps

* feat(presets): update Spec2Cloud version to 1.1.0 and adjust timestamps

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: update spec2cloud preset details and resolve merge conflicts

* fix: reorder Spec2Cloud entry in community presets for consistency

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-04 11:35:18 -05:00
Dyan Galih
521b0d9ef7 update security-review and memory-md extensions to latest versions (#2445)
* chore: update security-review extension to v1.4.2

* chore: update memory-md description and catalog updated_at
2026-05-04 10:07:58 -05:00
Nimra Akram
259494a328 fix: honor template overrides for tasks-template (#2278) (#2292)
* fix: honor template overrides for tasks-template (#2278)

- Add scripts/bash/setup-tasks.sh mirroring setup-plan.sh pattern
- Add scripts/powershell/setup-tasks.ps1 mirroring setup-plan.ps1 pattern
- Update tasks.md frontmatter to use dedicated setup-tasks scripts
- Resolve tasks template via override stack and emit path as TASKS_TEMPLATE in JSON output
- Reference resolved TASKS_TEMPLATE path in generate step instead of hardcoded path

* fix: remove stray EOF tokens from setup-tasks scripts

* fix: improve error messages for unresolved tasks-template

* test: update file inventory tests to include setup-tasks scripts

* fix: use Console::Error.WriteLine instead of Write-Error in setup-tasks.ps1

* fix: write prerequisite error messages to stderr in setup-tasks.ps1

* fix: validate tasks template is a file and normalize path in setup-tasks.ps1

* fix: improve tasks-template error message to mention full override stack

* test: add setup-tasks.sh to TestCopilotSkillsMode file inventory

* fix: skip feature-branch validation when feature.json pins FEATURE_DIR

* fix: correct override path in tasks-template error messages

* test: add integration tests for setup-tasks template resolution and branch validation

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: correct fixture paths and add spec.md prerequisite checks

* fix: use correct .registry schema in preset priority test

* fix: remove stale aaa-preset block and duplicate comment in preset priority test

* fix: align preset directory names with registry IDs in priority test

---------

Co-authored-by: Nimraakram22 <nimra.akram123451@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-01 16:18:19 -05:00
Chris Roberts
94074064c5 Add token-analyzer to community catalog (#2433)
Extension ID: token-analyzer
Version: 0.1.0
Author: Chris Roberts | coderandhiker
Description: Captures, analyzes, and compares token consumption across SDD workflows
Repository: https://github.com/coderandhiker/spec-kit-token-analyzer

Co-authored-by: Chris Roberts <chris@Chriss-MacBook-Pro.local>
2026-05-01 13:41:28 -05:00
Manfred Riem
f60e28ddba docs: add April 2026 newsletter (#2434) 2026-05-01 13:38:25 -05:00
Manfred Riem
822a0e5c61 feat: emit init-time notice for git extension default change (#2165) (#2432)
Add a non-blocking Panel notice during `specify init` when the git
extension auto-enables, informing users that starting in v0.10.0 this
will require explicit opt-in via `specify extension add git`.

- src/specify_cli/__init__.py: track successful git extension install
  and display yellow "Notice: Git Default Changing" panel
- tests/integrations/test_cli.py: integration test validating notice
  content (v0.10.0 timeline, opt-in messaging, migration command)
- docs/reference/core.md: user-facing NOTE about the upcoming change

Closes #2165
2026-05-01 13:06:42 -05:00
Dyan Galih
6546026626 Update DyanGalih(Memory Hub and Security Review) community extensions (#2429)
* chore: update DyanGalih extensions to latest versions

* chore: update catalog root timestamp to current
2026-05-01 11:55:55 -05:00
Pascal THUET
38fd1f6cc2 Support controlled multi-install for safe AI agent integrations (#2389)
* support controlled multi-install integrations

* fix: harden multi-install integration state

* refactor: isolate integration runtime helpers

* fix: address copilot review feedback

* fix: address follow-up copilot feedback

* fix: tighten integration switch semantics

* fix: address final copilot review feedback

* fix: harden integration manifest read errors

* fix: refuse symlinked shared infra paths

* test: filter expected self-test preset warning

* test: address copilot review nits

* refactor: centralize safe shared infra writes

* fix: use no-follow writes for shared infra

* fix: keep default integration atomic on template refresh

* fix: harden shared infra error paths

* fix: preflight shared infra and future state schemas

* fix: support nested shared scripts during preflight

* test: tolerate wrapped schema error output

* fix: use safe default mode for shared text writes

* fix: use posix paths in shared skip output

* fix: share project guard for integration use

* fix: centralize spec-kit project guards

* fix: use posix project paths in cli output

* fix: harden shared manifest and upgrade refresh
2026-05-01 11:54:41 -05:00
Pascal THUET
63cad6ace6 chore(integrations): clean up docs and project guard (#2428) 2026-05-01 10:33:22 -05:00
Manfred Riem
fcd6a80a07 chore: release 0.8.4, begin 0.8.5.dev0 development (#2431)
* chore: bump version to 0.8.4

* chore: begin 0.8.5.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-01 10:17:58 -05:00
Ismael
bb8fd50763 fix(specify): correct self-referencing step number in validation flow (#2152) 2026-05-01 10:13:31 -05:00
dependabot[bot]
cc6f203dd9 chore(deps): bump DavidAnson/markdownlint-cli2-action (#2425)
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.0.0 to 23.1.0.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](ce4853d438...6b51ade7a9)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 23.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-01 08:56:13 -05:00
Thorsten Hindermann
de9d98683a Add security-governance to community catalog (#2386)
Co-authored-by: Your Name <your@email.example>
2026-05-01 08:17:12 -05:00
Thorsten Hindermann
4133c8a543 Add cross-platform-governance to community catalog (#2384)
Co-authored-by: Your Name <your@email.example>
2026-05-01 08:03:12 -05:00
Thorsten Hindermann
6ee8a887e0 Add architecture-governance to community catalog (#2383)
Co-authored-by: Your Name <your@email.example>
2026-05-01 07:57:02 -05:00
Thorsten Hindermann
b13eea1e27 Add a11y-governance to community catalog (#2381)
Co-authored-by: Your Name <your@email.example>
2026-05-01 07:47:22 -05:00
Alex Vieira
9fac01fb47 feat(extensions): add Spec2Cloud extension for Azure deployment workflow (#2412)
* feat(extensions): add Spec2Cloud extension for Azure deployment workflow

Co-authored-by: Copilot <copilot@github.com>

* Update extensions/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update extensions/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat(extensions): update Spec2Cloud extension details and remove duplicate entry

Co-authored-by: Copilot <copilot@github.com>

* fix(extensions): correct formatting of updated_at timestamp

* fix(extensions): update Spec2Cloud extension version and timestamps

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-01 07:46:48 -05:00
Chengyou Liu
5edc9a5358 fix: migrate extension commands on integration switch (#2404)
* fix: migrate extension commands on integration switch

When switching integrations (e.g. kimi → opencode), extension commands
were not re-registered for the new agent, leaving the new agent without
extension support and orphaning files in the old agent's directory.

Changes:
- Add ExtensionManager.unregister_agent_artifacts() to clean up old
  agent extension files and registry entries during switch
- Add ExtensionManager.register_enabled_extensions_for_agent() to
  re-register all enabled extensions for the new agent
- Wire both into integration_switch() after uninstall/install phases
- Handle skills mode (Copilot --skills) correctly
- Add tests for kimi→opencode→claude migration, Copilot skills mode,
  and disabled extension handling

Fixes extension commands not appearing after integration switch.

* Update src/specify_cli/extensions.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-01 07:41:38 -05:00
Jeff Williams
da1bf028ab feat: add Squad Bridge extension to community catalog (#2417)
* feat: add squad bridge extension to community catalog

Adds spec-kit-squad by jwill824 — a Spec Kit extension that bootstraps
and synchronizes a Squad agent team from your Speckit spec and tasks.

- 4 commands: init, generate, route, status
- 2 hooks: after_specify (generate), after_tasks (route)
- v1.0.0 release

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: add requires.tools for squad-cli in catalog entry

* Update extensions/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 17:05:08 -05:00
Manfred Riem
7cedd85f2a chore: release 0.8.3, begin 0.8.4.dev0 development (#2418)
* chore: bump version to 0.8.3

* chore: begin 0.8.4.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-29 16:50:09 -05:00
Sakit
2cb848f0d3 Add Work IQ extension to community catalog (#2415)
* Add Work IQ extension to community catalog

Adds the Work IQ extension by sakitA to the community catalog.
Work IQ integrates Microsoft 365 organizational knowledge (emails,
meetings, documents, Teams) into spec-driven development workflows.

- 4 commands: ask, context, stakeholders, enrich
- 2 hooks: before_specify, after_specify
- Requires: speckit >=0.1.0, Node.js >=18.0.0, workiq CLI

Repository: https://github.com/sakitA/spec-kit-workiq

* Address PR review comments

- Fix download_url to use .zip (Spec Kit installer requires ZIP format)
- Bump top-level catalog updated_at to 2026-04-29

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Sakit Atakishiyev <satakishiyev@microsoft.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 16:41:19 -05:00
vishal-gandhi
237e918f11 feat(integrations): add Devin for Terminal skills-based integration (#2364)
* feat(integrations): add Devin for Terminal skills-based integration

- Register DevinIntegration as a SkillsIntegration with .devin/skills/ layout
- Add catalog entry, docs row, and supported-agents listing
- Display /speckit-<command> hyphen syntax in init "Next Steps" panel
  (matches Claude/Cursor/Copilot skills mode, since Devin invokes skills
  by directory name)

Closes #2346

* fix(devin): implement -p non-interactive dispatch; clarify skills comment

Addresses Copilot review on PR #2364:

- Override build_exec_args() in DevinIntegration to emit
  'devin -p <prompt> [--model X]' for non-interactive text dispatch
  (verified Devin CLI supports -p / --print). Returns None when
  output_json=True since Devin has no structured-output flag, so
  CommandStep workflows that require JSON cleanly raise
  NotImplementedError instead of crashing on an unknown CLI flag.
  requires_cli=True is retained for tool detection.

- Extend the skills-integrations enumeration comment in
  specify_cli/__init__.py to include copilot and devin so the
  comment matches the code below it.

* fix(devin): always return exec args; document plain-text stdout

Addresses third Copilot review comment on PR #2364.

Returning None from build_exec_args() when output_json=True
incorrectly used the codebase's IDE-only sentinel: workflow
CommandStep checks 'impl.build_exec_args("test") is None' to
detect non-dispatchable integrations (test_workflows.py exercises
this with WindsurfIntegration). The previous implementation made
Devin appear non-dispatchable to all command steps even though it
runs fine via 'devin -p'.

Always return the args list. When output_json is requested, Devin
is still dispatched and returns plain-text stdout instead of
structured JSON; the docstring documents this explicitly.

* docs(devin): include claude in skills-integrations enumeration comment

Addresses Copilot review on PR #2364: the comment listing skills
integrations omitted Claude, which is also a SkillsIntegration
subclass. Updated to keep the comment accurate for future readers.

* test(devin): add build_exec_args regression tests; bump catalog updated_at

Addresses Copilot review on PR #2364, per @mnriem's request to
'address the Copilot feedback, especially the testing ask':

- tests/integrations/test_integration_devin.py: add TestDevinBuildExecArgs
  with three regression assertions:
    * build_exec_args returns args (not the None IDE-only sentinel)
    * --output-format is never emitted, regardless of output_json
    * --model flag is passed through correctly
- integrations/catalog.json: bump top-level updated_at to reflect the
  Devin entry addition so downstream catalog consumers can detect the
  change reliably.
2026-04-29 16:22:06 -05:00
Quratulain-bilal
ab9c70262d fix: include --from git+... in upgrade hint to avoid PyPI squat package (#2411)
The compatibility-error messages in extensions.py and presets.py, plus the
extension troubleshooting guide, told users to upgrade with:

    uv tool install specify-cli --force

Without `--from git+https://github.com/github/spec-kit.git`, uv resolves
`specify-cli` from PyPI, where an unrelated package with the same name
(no author, no project URLs) ships a stub CLI that lacks `extension`,
`preset`, and most spec-kit commands. Users following the upgrade hint
land on the squat package and report "extension command removed"
(see #1982).

Reuse the existing `REINSTALL_COMMAND` constant in extensions.py and
import it from presets.py so all three call sites point at the GitHub
source. The doc fix also adds a one-line note explaining the PyPI
collision so the same advice doesn't get re-stripped later.

Refs #1982
2026-04-29 10:12:04 -05:00
Andrii Furmanets
c079b2cc32 fix: dispatch opencode commands via run (#2410) 2026-04-29 09:39:45 -05:00
Adrian Osorio Blanchard
1049e17a43 feat: add catalog discovery CLI commands (#2360)
* feat: add catalog discovery CLI commands

* fix: address second Copilot review

* fix: address third Copilot review

* fix: align catalog remove with displayed order

* fix: route local catalog config errors to local guidance

* fix: address integration catalog review feedback

* fix: accept numeric string catalog priorities

* fix: align catalog remove with visible entries

* fix: preserve invalid catalog root validation

* fix: include invalid catalog priority value

* fix: preserve falsy catalog root validation

* fix: clarify integration catalog guidance

* fix: align integration catalog list and remove

* fix: align integration catalog edge cases

* fix: clarify catalog error guidance tests

* fix: clarify integration catalog edge cases

* fix: harden integration catalog removal

* fix: validate integration state before catalog search

* fix: reject empty integration catalog URL

* fix: allow catalog remove to clean non-string URLs

* fix: address catalog env and priority review

* fix: align catalog source display names

* fix: align catalog fallback names
2026-04-29 07:24:30 -05:00
Dyan Galih
9cf3151a72 update security review extension catalog to v1.3.0 (#2374)
* chore: update security review catalog metadata

* fix: sync security review catalog with v1.3.0

* chore: refresh community catalog timestamp

* fix: update author information for Security Review catalog entry

* fix: correct author name format in Security Review catalog entry

* chore: refresh community catalog timestamps

* chore: reapply catalog formatting

* chore: align catalog formatting with main
2026-04-29 07:16:01 -05:00
Leonardo Nascimento
9483e5cb1f chore(catalog): bump v-model extension to v0.6.0 (#2399)
Update v-model extension entry in community catalog to reflect the v0.6.0
release: https://github.com/leocamello/spec-kit-v-model/releases/tag/v0.6.0

Highlights of v0.6.0:
- Domain Overlay Architecture (9 overlay manifests; automotive, medical,
  aerospace, general)
- ID Lifecycle Model (Proposed -> Active -> Deprecated -> Removed)
- Standards enrichment across all 11 commands (IEEE 1012:2016, ISO
  25010:2023, ISO 42030:2019, ISO 12207:2017, IEEE 1016, IEEE 29148,
  ISO 29119-4, ISO 14971, DO-178C, ARP4761A, INCOSE SE Handbook)
- Aerospace DO-178C support: Flight Warning Computer DAL-A golden fixture
- Test infrastructure: fixtures reorganized; +11 LLM-as-judge evals (42 -> 53)

Command count remains 14 (no new commands added in this release).
Stars updated to live count (21) from GitHub API.
2026-04-28 17:14:21 -05:00
NaviaSamal
38f99e8381 feat: add threatmodel extension to community catalog (#2369)
* feat: add threatmodel extension to community catalog

* update timestamp for catalogue freshness

* update timestamp for catalogue freshness

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Update README.md

update readme.md with spec-kit-threatmodel

---------

Co-authored-by: Samal <navia.samal@sap.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 17:02:42 -05:00
Thorsten Hindermann
16aa57fce4 Add isaqb-architecture-governance to community catalog (#2385)
Co-authored-by: Your Name <your@email.example>
2026-04-28 15:34:53 -05:00
Manfred Riem
bc3409e340 chore: release 0.8.2, begin 0.8.3.dev0 development (#2397)
* chore: bump version to 0.8.2

* chore: begin 0.8.3.dev0 development

* Update CHANGELOG.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 13:52:25 -05:00
Ben Buttigieg
0aa588a9b4 Merge pull request #2392 from BenBtg/community/add-m365-extension
Add m365 to community catalog
2026-04-28 17:06:54 +01:00
Ben Buttigieg
ea92155b52 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:55:12 +01:00
Manfred Riem
047be2308c Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 10:50:23 -05:00
Ben Buttigieg
7d0f670b83 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:40:27 +01:00
Ben Buttigieg
5b3ebabcaf Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:36:01 +01:00
Ben Buttigieg
719eef3ff1 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:31:53 +01:00
Ben Buttigieg
fe9f19d569 Potential fix for pull request finding
"microsoft-365",
        "teams",
        "meetings",
        "transcripts",

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:16:47 +01:00
Ben Buttigieg
56f9b95b0d Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:07:20 +01:00
Ben Buttigieg
7b99fef2bc Merge branch 'main' into community/add-m365-extension 2026-04-28 15:51:40 +01:00
Ben Buttigieg
bd3ae9aaef Add MarkItDown Document Converter extension to community catalog (#2390) 2026-04-28 09:28:05 -05:00
Ben Buttigieg
a0634ef96e Merge branch 'github:main' into community/add-m365-extension 2026-04-28 15:18:50 +01:00
adaumann
a918979236 feat: Speckit preset fiction book v1.7 - Support for RAG (Chroma DB) offline semantic search (#2367)
* Update preset-fiction-book-writing to community catalog

- Preset ID: fiction-book-writing
- Version: 1.5.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for novel and long-form fiction. Replaces software engineering terminology with storytelling craft: specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports 8 POV modes, all major plot structure frameworks, 5 humanized-AI prose profiles, and exports to DOCX/EPUB/LaTeX via pandoc. V1.5.0: Support interactive, audiobooks, series, workflow corrections

* Add fiction-book-writing preset to community catalog

- Preset ID: fiction-book-writing
- Version: 1.6.0
- Author: Andreas Daumann
- Description: Added support for 12 languages, export with templates, cover builder, bio builder, workflow fixes

* Update presets/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fixed update_at for fiction-book-writing preset

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fixed description for fiction-book-writing

* "Add fiction-book-preset to community catalog

- Preset ID: fiction-book-writing
- Version: 1.7.0
- Author: Andreas Daumann
- Description: It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. V1.7.0: Support for offline semantic search.

* Update presets/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update presets/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add fiction-book-writing to community catalog

- Preset ID: fiction-book-writing
- Version: 1.7.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for novel and long-form fiction. RAG support

* Update docs/community/presets.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 08:58:30 -05:00
Quratulain-bilal
3a7f64c8a5 fix(extensions): use explicit UTF-8 encoding when reading manifest YAML (#2370)
* fix(extensions): use explicit UTF-8 encoding when reading manifest YAML

On Windows, Python's open() defaults to the system locale encoding
(e.g., GBK on Chinese Windows), which causes UnicodeDecodeError when
extension.yml or preset.yml contains non-ASCII content such as Chinese
characters in description fields.

Add encoding='utf-8' to ExtensionManifest._load_yaml and
PresetManifest._load_yaml so manifests are read consistently across
platforms.

Fixes #2325

* test(extensions,presets): add UTF-8 manifest regression tests for #2325

Positive: extension.yml/preset.yml with non-ASCII (Chinese + emoji)
descriptions load correctly when written as UTF-8 bytes — fails on
Windows without explicit encoding='utf-8'.

Negative: files containing invalid UTF-8 bytes raise a clean error
(ValidationError or UnicodeDecodeError), not a silent crash.

* fix(extensions,presets): wrap I/O and decode errors as ValidationError

Address remaining Copilot concerns on #2370:

- Catch UnicodeDecodeError and OSError in both manifest loaders and
  re-raise as ValidationError / PresetValidationError so callers see a
  consistent error type, not a bare decode/IO traceback.
- Validate that PresetManifest YAML root is a mapping (extensions.py
  already had this; presets.py was missing it). Treat None as {} for
  empty-file compatibility.
- Tighten the negative regression tests to assert the specific message,
  and add a non-mapping-root test for PresetManifest matching the
  existing one for ExtensionManifest.
2026-04-28 08:47:22 -05:00
Ben Buttigieg
77ca5f4ed5 catalog: add m365 community extension
Add Microsoft 365 Integration to community catalog and README.
Ingests Teams messages, files, and meeting transcripts as Markdown
for use with speckit specify.
2026-04-27 17:54:50 +01:00
Manfred Riem
171b65ac33 docs: replace deprecated --ai flag with --integration in all documentation (#2359)
* docs: replace deprecated --ai flag with --integration in all documentation

Replace all user-facing --ai, --ai-skills, and --ai-commands-dir references
with their modern equivalents:

- --ai <agent> → --integration <agent>
- --ai-skills → --integration-options="--skills"
- --ai-commands-dir <dir> → --integration generic --integration-options="--commands-dir <dir>"

Updated files:
- README.md (~17 occurrences)
- docs/installation.md (~8 occurrences)
- docs/upgrade.md (~11 occurrences)
- docs/local-development.md (~5 occurrences)
- CONTRIBUTING.md (1 occurrence)
- extensions/EXTENSION-USER-GUIDE.md (1 occurrence)
- src/specify_cli/__init__.py (docstring examples and error messages)

Left unchanged:
- CHANGELOG.md (historical record)
- Test files (intentionally exercise deprecated flag path)
- CLI flag implementation (backward compatibility)

Closes #2358

* docs: address review feedback on pre-existing issues

- Fix duplicate copilot example in README.md (replace with codex)
- Fix invalid gemini --integration-options="--skills" example (gemini
  does not support --skills)
- Update generic integration comment from 'Unsupported agent' to
  'Bring your own agent; requires --commands-dir'
- Clarify EXTENSION-USER-GUIDE.md: skills auto-register for
  skills-based integrations, not only with --integration-options

* docs: replace bare 'AI agent' / 'AI assistant' with 'coding agent' throughout

Full sweep across all documentation and user-facing CLI messages to
align terminology. Bare references like 'AI agent', 'AI assistant',
and 'AI Agent' are replaced with 'coding agent' or 'coding agent
integration' as appropriate.

Intentionally left unchanged:
- 'AI coding agent' (already correct expanded form)
- Deprecated --ai flag help text and error messages (describes the
  deprecated flag itself)
- Community extension descriptions (external project text)
- 'generated by an AI' in CONTRIBUTING.md (general AI, not agent)

* docs: address review — remove deprecated --offline, qualify --skills scope

- Remove --offline from docstring examples (deprecated no-op)
- Remove --offline from CONTRIBUTING.md testing example
- Replace --offline instructions in docs/installation.md with note that
  bundled assets are used by default
- Qualify --integration-options="--skills" in README.md to note it only
  applies to integrations that support skills mode
2026-04-24 16:04:04 -05:00
Taylor Mulder
232c19cb04 feat(extensions,presets): authenticate GitHub-hosted catalog and download requests with GITHUB_TOKEN/GH_TOKEN (#2331)
* feat(extensions,presets): authenticate GitHub-hosted catalog and download requests with GITHUB_TOKEN/GH_TOKEN

Squashed from #2087 (original author: @anasseth).

Adds GitHub-token authentication to extension and preset catalog fetching
and ZIP downloads so private GitHub repos work when GITHUB_TOKEN/GH_TOKEN
is set, while preventing credential leakage to non-GitHub hosts.

- Introduces shared _github_http module with build_github_request() and
  open_github_url() helpers
- Routes ExtensionCatalog and PresetCatalog network calls through
  GitHub-auth-aware opener
- Adds comprehensive unit/integration tests for auth header behavior
- Updates user docs for both extensions and presets

Co-authored-by: anasseth <16745089+anasseth@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(auth): address review feedback from #2087

- Fix redirect handler to preserve Authorization on GitHub-to-GitHub
  redirects (e.g. github.com → codeload.github.com). The previous
  implementation relied on super().redirect_request() which strips
  auth on cross-host redirects, breaking private repo archive downloads.
- Add codeload.github.com to documented host lists in both
  EXTENSION-USER-GUIDE.md and presets/README.md
- Add redirect auth-preservation and auth-stripping tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(auth): use Bearer scheme instead of token for consistency

Aligns with the rest of the codebase (e.g. __init__.py:1721) and
GitHub's current API guidance. Updates all test assertions accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: address second round of Copilot review feedback

- Fix docstring to say Bearer instead of token (matches implementation)
- Remove unused imports/fixtures from redirect tests (GITHUB_HOSTS,
  MagicMock, temp_dir, monkeypatch)
- Replace __import__('io').BytesIO() with normal import io pattern
  in test_presets.py

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: anasseth <16745089+anasseth@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-24 14:17:40 -05:00
Manfred Riem
ca51d739fb Update extensify to v1.1.0 in community catalog (#2337) 2026-04-24 13:58:34 -05:00
Manfred Riem
03f3024c66 feat(init): deprecate --no-git flag, gate deprecations at v0.10.0 (#2357)
* feat(init): deprecate --no-git flag, gate deprecations at v0.10.0

- Add deprecation warning when --no-git is used on specify init
- Update --ai deprecation gate from 1.0.0 to 0.10.0
- Update test expectation for the new version gate

Closes #2167

* fix: address PR review feedback

- Update --no-git deprecation message to reference existing 'specify extension'
  commands instead of non-existent --extension flag
- Add test_no_git_emits_deprecation_warning CLI test

* fix: strengthen --no-git deprecation test assertions

Add assertions unique to the --no-git message ('will be removed',
'git extension will no longer be enabled by default') to prevent
false positives from the --ai deprecation panel.
2026-04-24 13:54:40 -05:00
Quratulain-bilal
aad7b16188 Add Spec Orchestrator extension to community catalog (#2350) 2026-04-24 13:11:39 -05:00
Manfred Riem
6cec171772 chore: release 0.8.1, begin 0.8.2.dev0 development (#2356)
* chore: bump version to 0.8.1

* chore: begin 0.8.2.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-24 12:50:38 -05:00
Adrian Osorio Blanchard
37745ec2ee fix(plan): use .specify/feature.json to allow /speckit.plan on custom git branches (#2305) (#2349)
* fix: allow plan setup to use feature metadata on custom branches

* fix: harden feature metadata validation

* fix: use portable feature metadata path

Made-with: Cursor

* fix: share feature.json parser and make path compare OS aware

* test: isolate setup plan subprocess environment

* fix: normalize feature metadata paths with pwd -P
2026-04-24 12:38:13 -05:00
Rodolphe Lefebvre
998f927576 feat(vibe): migrate to SkillsIntegration from the old prompts-based MarkdownIntegration (#2336)
* feat(vibe): migrate to SkillsIntegration and inject user-invocable frontmatter
Switches VibeIntegration from the old prompts-based MarkdownIntegration to SkillsIntegration, adopting the .vibe/skills/speckit-<name>/SKILL.md layout required by Mistral Vibe v2.0.0+. Post-processes each generated SKILL.md to inject `user-invocable: true` so skills are directly callable by users, not  just by other agents.

* test(vibe): assert user- invocable: true is present in all generated SKILL.md files

* Update tests/integrations/test_integration_vibe.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-24 12:37:05 -05:00
Manfred Riem
9f14dfc6c6 docs: move community presets table to docs site, add missing entries (#2341)
* docs: move community presets table to docs site, add missing entries

- Move the full community presets table from README.md to the docs site
  at docs/community/presets.md, replacing the README section with a
  short link (matching the pattern used for Walkthroughs and Friends).
- Add missing Jira Issue Tracking and Screenwriting rows to the docs
  table so it reflects all entries in catalog.community.json.

* docs(presets): add docs site table step to publishing guide

Add step to update docs/community/presets.md when submitting a
community preset, and add corresponding PR checklist item. Matches
the pattern used in the extensions publishing guide.

* Clarify alphabetical sort key in presets publishing guide

Specify that the docs table should be sorted by preset name (the first
column), disambiguating from the catalog JSON which sorts by preset ID.

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Address review: fix provides count, admonition style, example row

- Add missing scripts count to Fiction Book Writing table row to match catalog
- Switch README disclaimer to GitHub admonition format for consistency
- Include optional scripts count in PUBLISHING.md example row

* Fix Fiction Book Writing link text to match actual repo name

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-24 11:03:10 -05:00
Manfred Riem
8750e94d10 docs(presets): add lean preset README and enrich catalog metadata (#2340)
* docs(presets): add lean preset README and enrich catalog metadata

- Add README.md documenting the lean workflow preset, its commands,
  when to use it, and development instructions.
- Add license, requires.speckit_version, and provides.commands fields
  to the lean preset catalog entry.
- Add "core" tag to preset.yml for discoverability.

* fix: bump catalog updated_at and add provides.templates for consistency

Address PR review feedback:
- Bump updated_at to reflect catalog modification time
- Add provides.templates (0) to lean preset entry for consistency
  with catalog schema used in catalog.community.json
2026-04-24 10:06:39 -05:00
Manfred Riem
52c0a5f88f fix: resolve command references per integration type (dot vs hyphen) (#2354)
* fix: resolve command references per integration type (dot vs hyphen)

Replace hardcoded /speckit.<cmd> references in templates with
__SPECKIT_COMMAND_<NAME>__ placeholders that are resolved at
setup time based on the integration type:

- Markdown/TOML/YAML agents: separator='.' → /speckit.plan
- Skills agents: separator='-' → /speckit-plan

Changes:
- Add resolve_command_refs() static method to IntegrationBase
- Add invoke_separator class attribute (.  for base, - for skills)
- Wire into process_template() as step 8
- Update _install_shared_infra() to process page templates
- Replace /speckit.* in 5 command templates and 3 page templates
- Add unit tests for resolve_command_refs (positive + negative)
- Add integration tests verifying on-disk content for all agents
- Add end-to-end CLI tests for Claude (skills) and Copilot (markdown)

Fixes #2347

* review: use effective_invoke_separator() for Copilot skills mode

Address PR review feedback: instead of bleeding _skills_mode
knowledge into the CLI layer, add effective_invoke_separator()
method to IntegrationBase that accepts parsed_options.

CopilotIntegration overrides it to return "-" when skills
mode is requested. The CLI layer simply asks the integration
for its separator — no hasattr or _skills_mode coupling.

Also adds tests for the new method on both base and Copilot,
plus an end-to-end test for 'specify init --integration copilot
--integration-options --skills' verifying page templates get
hyphen refs.

* fix: build_command_invocation preserves full suffix for extension commands

Previously rsplit('.', 1)[-1] on 'speckit.git.commit' yielded
just 'commit', producing /speckit.commit instead of
/speckit.git.commit (or /speckit-git-commit for skills).

Fix: strip only the 'speckit.' prefix when present, then join
remaining segments with the appropriate separator.

Updated in IntegrationBase, SkillsIntegration, and
CopilotIntegration. Added tests for extension commands in
build_command_invocation across all three.

* fix: Copilot dispatch_command() preserves full extension command suffix

dispatch_command() had the same rsplit('.', 1)[-1] bug as
build_command_invocation() — speckit.git.commit would dispatch
as /speckit-commit instead of /speckit-git-commit in skills
mode, or --agent speckit.commit instead of speckit.git.commit
in default mode.
2026-04-24 10:04:14 -05:00
Valentyn
6413414907 Update product-forge to v1.5.1 in community catalog (#2352)
* Update product-forge to v1.5.0 in community catalog

- Extension ID: product-forge
- Version: 1.1.1 → 1.5.0
- Author: VaiYav
- Description: updated to reflect v1.5 features (portfolio, lite mode,
  monorepo, optional V-Model)
- Commands: 10 → 29
- Tags: refreshed to reflect current surface area
- download_url pinned to v1.5.0 release tag
- updated_at bumped to 2026-04-24

Release: https://github.com/VaiYav/speckit-product-forge/releases/tag/v1.5.0

* Bump product-forge to v1.5.1 (docs patch)

Follow-up to v1.5.0 that surfaces the optional V-Model dependency
(leocamello/spec-kit-v-model ≥ 0.5.0) in README Requirements,
config-template.yml, and docs/config.md. Docs-only patch — no
behavioural change.

Release: https://github.com/VaiYav/speckit-product-forge/releases/tag/v1.5.1
2026-04-24 09:48:50 -05:00
dependabot[bot]
7f708b9e6f chore(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 (#2345)
Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.0.0 to 8.1.0.
- [Release notes](https://github.com/astral-sh/setup-uv/releases)
- [Commits](cec208311d...08807647e7)

---
updated-dependencies:
- dependency-name: astral-sh/setup-uv
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 07:36:23 -05:00
Manfred Riem
13d88d22a6 fix: replace xargs trim with sed to handle quotes in descriptions (#2351)
xargs re-parses stdin as shell tokens, causing 'unterminated quote'
errors when feature descriptions contain apostrophes, double quotes,
or backslashes. Replace with sed-based whitespace trim that preserves
input verbatim.

Add regression tests for special characters in descriptions (core and
extension scripts), plus a negative test for whitespace-only input.

Fixes #2339
2026-04-24 07:13:36 -05:00
Ed Harrod
6bf4ebbe33 feat: register jira preset in community catalog (#2224)
* feat: register jira preset in community catalog

Adds luno/spec-kit-preset-jira — overrides speckit.taskstoissues to
create Jira issues instead of GitHub Issues.

See #2223 for context on why this is a preset rather than an extension.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use immutable tag URL and sort jira preset alphabetically

- Change download_url from heads/main to refs/tags/v1.0.0 for reproducible installs
- Move jira entry to correct alphabetical position in presets object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Ed Harrod <1381991+echarrod@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 14:32:30 -05:00
adaumann
5a52b7623e feat: Preset screenwriting (#2332)
* Update preset-fiction-book-writing to community catalog

- Preset ID: fiction-book-writing
- Version: 1.5.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for novel and long-form fiction. Replaces software engineering terminology with storytelling craft: specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports 8 POV modes, all major plot structure frameworks, 5 humanized-AI prose profiles, and exports to DOCX/EPUB/LaTeX via pandoc. V1.5.0: Support interactive, audiobooks, series, workflow corrections

* Add screenwriting preset to community catalog

- Preset ID: screenwriting
- Version: 1.0.0
- Author: Andreas Daumann
- Description: Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Speckit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents replace prose fiction conventions. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks.

* Update presets/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update presets/catalog.community.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 14:24:29 -05:00
Manfred Riem
89fc554ce5 chore: release 0.8.0, begin 0.8.1.dev0 development (#2333)
* chore: bump version to 0.8.0

* chore: begin 0.8.1.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-23 10:12:09 -05:00
Copilot
a067d4c2e3 feat(presets): Composition strategies (prepend, append, wrap) for templates, commands, and scripts (#2133)
* fix: rebase onto upstream/main, resolve conflicts with PR #2189

upstream/main merged PR #2189 (wrap-only strategy) which overlaps with
our comprehensive composition strategies (prepend/append/wrap). Resolved
conflicts keeping our implementation as source of truth:

- README: keep our future considerations (composition is now fully
  implemented, not a future item)
- presets.py: keep our composition architecture (_reconcile_composed_commands,
  collect_all_layers, resolve_content) while preserving #2189's
  _substitute_core_template which is used by agents.py for skill
  generation
- tests: keep both test sets (our composition tests + #2189's wrap
  tests), removed TestReplayWrapsForCommand and
  TestInstallRemoveWrapLifecycle which test the superseded
  _replay_wraps_for_command API; our composition tests cover equivalent
  scenarios
- Restored missing _unregister_commands call in remove() that was lost
  during #2189 merge

* fix: re-create skill directory in _reconcile_skills after removal

After _unregister_skills removes a skill directory, _register_skills
skips writing because the dir no longer passes the is_dir() check.
Fix by ensuring the skill subdirectory exists before calling
_register_skills so the next winning preset's content gets registered.

Fixes the Claude E2E failure where removing a top-priority override
preset left skill-based agents without any SKILL.md file.

* fix: address twenty-third round of Copilot PR review feedback

- Protect reconciliation in remove(): wrap _reconcile_composed_commands
  and _reconcile_skills in try/except so failures emit a warning instead
  of leaving the project in an inconsistent state
- Protect reconciliation in install(): same pattern for post-install
  reconciliation so partial installs don't lack cleanup
- Inherit scripts/agent_scripts from base frontmatter: when composing
  commands, merge scripts and agent_scripts keys from the base command's
  frontmatter into the top layer's frontmatter if missing, preventing
  composed commands from losing required script references
- Add tier-5 bundled core fallback to collect_all_layers(): check the
  bundled core_pack (wheel) or repo-root templates (source checkout) when
  .specify/templates/ doesn't contain the core file, matching resolve()'s
  tier-5 fallback so composition can always find a base layer

* fix: address twenty-fourth round of Copilot PR review feedback

- Use yaml.safe_load for frontmatter parsing in resolve_content instead
  of CommandRegistrar.parse_frontmatter which uses naive find('---',3);
  strip strategy key from final frontmatter to prevent leaking internal
  composition directives into rendered agent command files
- Filter _reconcile_skills to specific commands: use _FilteredManifest
  wrapper so only the commands being reconciled get their skills updated,
  preventing accidental overwrites of other commands' skills that may be
  owned by higher-priority presets

* fix: address twenty-fifth round of Copilot PR review feedback

- Support legacy command-frontmatter strategy: when preset.yml doesn't
  declare a strategy, check the command file's YAML frontmatter for
  strategy: wrap as a fallback so legacy wrap presets participate in
  composition and multi-preset chaining
- Guard skill dir creation in _reconcile_skills: only re-create the
  skill directory if the skill was previously managed (listed in some
  preset's registered_skills), avoiding creation of new skill dirs
  that _register_skills would normally skip

* fix: add explanatory comment to empty except in legacy frontmatter parsing

* fix: address twenty-sixth round of Copilot PR review feedback

- Unregister stale commands when composition fails: when resolve_content
  returns None during reconciliation (base layer removed), unregister
  the command from non-skill agents and emit a warning
- Load extension aliases during reconciliation: _register_command_from_path
  now checks extension.yml for aliases when the winning layer is an
  extension, so alias files are restored after preset removal
- Use line-based fence detection for legacy frontmatter strategy fallback:
  scan for --- on its own line instead of split('---',2) to avoid
  mis-parsing YAML values containing ---

* fix: address twenty-seventh round of Copilot PR review feedback

- Handle non-preset winners in _reconcile_skills: when the winning
  layer is core/extension/project-override, restore skills via
  _unregister_skills so skill-based agents stay consistent with the
  priority stack
- Update base_frontmatter_text on replace layers: when a higher-priority
  replace layer occurs during composition, update both top and base
  frontmatter so scripts/agent_scripts inheritance reflects the
  effective base beneath the top composed layer

* fix: address twenty-eighth round of Copilot PR review feedback

- Parse only interior lines in _parse_fm_yaml: use lines[1:-1] instead
  of filtering all --- lines, preventing corruption when YAML values
  contain a line that is exactly ---
- Omit empty frontmatter: skip re-rendering when top_fm is empty dict
  to avoid emitting ---/{}/--- for intentionally empty frontmatter
- Update scaffold wrap comment: mention both {CORE_TEMPLATE} and
  $CORE_SCRIPT placeholders for templates/commands vs scripts
- Clarify shell composition scope in ARCHITECTURE.md: note that bash/PS1
  resolve_template_content only handles templates; command/script
  composition is handled by the Python resolver

* fix: address twenty-ninth round of Copilot PR review feedback

- Fix TestCollectAllLayers docstring: reference collect_all_layers()
- Add default/unknown strategy handling in bash/PS1 composition: error
  on unrecognized strategy values instead of silently skipping
- Fix comment: .composed/ is a persistent dir, not temporary
- Fix comment: legacy fallback checks all valid strategies, not just wrap
- Cache PresetRegistry in _reconcile_skills: build presets_by_priority
  once instead of constructing registry per-command

* fix: address thirtieth round of Copilot PR review feedback

- Guard legacy frontmatter fallback: only check command file frontmatter
  for strategy when the manifest entry doesn't explicitly include the
  strategy key, preventing override of manifest-declared strategies
- Document rollback limitation: note that mid-registration failures may
  leave orphaned agent command files since partial progress isn't
  captured by the local vars

* fix: handle project override skills and extension context in reconciliation

* fix: add comment to empty except in extension registration fallback

* fix: filter extension commands in reconciliation and fix type annotation

* fix: filter extension commands from post-install reconciliation

Apply the same extension-installed check used in _register_commands to
the reconciliation command list, preventing reconciliation from
registering commands for extensions that are not installed.

* fix: skip convention fallback for explicit file paths and add stem fallback to tier-5

When a preset manifest provides an explicit file path that does not
exist, skip the convention-based fallback to avoid masking typos.
Also add speckit.<stem> to <stem>.md fallback in tier-5 bundled/source
core lookup for consistency with tier-4.

* fix: scan past non-replace layers to find base in resolve_content

The base-finding scan now skips non-replace layers below a replace
layer instead of stopping at the first non-replace. This fixes the
case where a low-priority append/prepend layer sits below a replace
that should serve as the base for composition.

* fix: add context_note to non-skill agent registration for extensions

Add context_note parameter to register_commands_for_non_skill_agents
and pass extension name/id during reconciliation so rendered command
files preserve the extension-specific context markers.

* fix: Optional type, rollback safety, and override skill restoration

- Fix context_note type to Optional[str]
- Wrap shutil.rmtree in try/except during install rollback
- Separate override-backed skills from core/extension in _reconcile_skills

* fix: align bash/PS1 base-finding with Python resolver

Rewrite bash and PowerShell composition loops to find the effective
base replace layer first (scanning bottom-up, skipping non-replace
layers below it), then compose only from the base upward. This
prevents evaluation of irrelevant lower layers (e.g. a wrap with
no placeholder below a replace) and matches resolve_content behavior.

* fix: PS1 no-python warning, integration hook for override skills, alias cleanup

- Warn when no Python 3 found in PS1 and presets use composition strategies
- Apply post_process_skill_content integration hook when restoring
  override-backed skills so agent-specific flags are preserved
- Unregister command aliases alongside primary name when composition
  fails to prevent orphaned alias files

* fix: include aliases in removed_cmd_names during preset removal

Read aliases from preset manifest before deleting pack_dir so alias
command files are included in unregistration and reconciliation.

* fix: add comment to empty except in alias extraction during removal

* fix: scan top-down for effective base in all resolvers

Change base-finding to scan from highest priority downward to find the
nearest replace layer, then compose only layers above it. Prevents
evaluation of irrelevant lower layers (e.g. a wrap without placeholder
below a higher-priority replace) across Python, bash, and PowerShell.

* fix: align CLI composition chain display with top-down base-finding

Show only contributing layers (base and above) in preset resolve
output, matching resolve_content top-down semantics. Layers below
the effective base are omitted since they do not contribute.

* fix: guard corrupted registry entries and make manifest authoritative

- Add isinstance(meta, dict) guard in bash registry parsing so corrupted
  entries are skipped instead of breaking priority ordering
- Only use convention-based file lookup when the manifest does not list
  the requested template, making preset.yml authoritative and preventing
  stray on-disk files from creating unintended layers

* fix: align resolve() with manifest file paths and match extension context_note

- Update resolve() preset tier to consult manifest file paths before
  convention-based lookup, matching collect_all_layers behavior
- Use exact extension context_note format matching extensions.CommandRegistrar
- Update test to declare template in manifest (authoritative manifest)

* revert: restore resolve() convention-based behavior for backwards compatibility

resolve() is the existing public API used by shell scripts and other
callers. Changing it to manifest-authoritative breaks backward compat
for presets that rely on convention-based file lookup. Only the new
collect_all_layers/resolve_content path uses manifest-authoritative
logic.

* fix: only pre-compose when this preset is the top composing layer

Skip composition in _register_commands when a higher-priority replace
layer already wins for the command. Register the raw file instead and
let reconciliation write the correct final content.

* fix: deduplicate PyYAML warnings and use self.registry in reconciliation

- Emit PyYAML-missing warning once per function call in bash/PS1 instead
  of per-preset to avoid spamming stderr
- Use self.registry.list_by_priority() in reconciliation methods instead
  of constructing new PresetRegistry instances to avoid redundant I/O
  and potential consistency issues

* fix: document strategy handling consistency between layers and registrar

Composed output already strips strategy from frontmatter (resolve_content
pops it). Raw file registration preserves legacy frontmatter strategy
for backward compat; reconciliation corrects the final state.

* fix: correct stale comments for alias tracking and base-finding algorithm

* security: validate manifest file paths in bash/PowerShell resolvers

Reject absolute paths and parent directory traversal (..) in the
manifest-declared file field before joining with the preset directory.
Matches the Python-side validation in PresetManifest._validate().

---------

Co-authored-by: Manfred Riem <15701806+mnriem@users.noreply.github.com>
2026-04-23 10:07:52 -05:00
Copilot
8fefd2a532 feat(copilot): support --integration-options="--skills" for skills-based scaffolding (#2324)
* Initial plan

* feat(copilot): add --skills flag for skills-based scaffolding

Add --skills integration option to CopilotIntegration that scaffolds
commands as speckit-<name>/SKILL.md under .github/skills/ instead of
the default .agent.md + .prompt.md layout.

- Add options() with --skills flag (default=False)
- Branch setup() between default and skills modes
- Add post_process_skill_content() for Copilot-specific mode: field
- Adjust build_command_invocation() for skills mode (/speckit-<stem>)
- Update dispatch_command() with skills mode detection
- Parse --integration-options during init command
- Add 22 new skills-mode tests
- All 15 existing default-mode tests continue to pass

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/a4903fab-64ff-46c3-8eb8-a47f495a70c0

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* docs(AGENTS.md): document Copilot --skills option

Agent-Logs-Url: https://github.com/github/spec-kit/sessions/a4903fab-64ff-46c3-8eb8-a47f495a70c0

Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>

* Potential fix for pull request finding 'Unused local variable'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* fix: address PR #2324 review feedback

- Reset _skills_mode at start of setup() to prevent singleton state leak
- Tighten skills auto-detection to require speckit-*/SKILL.md (not any
  non-empty .github/skills/ directory)
- Add copilot_skill_mode to init next-steps so skills mode renders
  /speckit-plan instead of /speckit.plan
- Fix docstring quoting to match actual unquoted output
- Add 4 tests covering singleton reset, auto-detection false positive,
  speckit layout detection, and next-steps skill syntax
- Fix skipped test_invalid_metadata_error_returns_unknown by simulating
  InvalidMetadataError on Python versions that lack it

* fix: inline skills prompt in dispatch_command auto-detection path

build_command_invocation() reads self._skills_mode which stays False
when skills mode is only auto-detected from the project layout. Inline
the /speckit-<stem> prompt construction so dispatch_command() sends the
correct prompt regardless of how skills mode was detected.

Also strengthen test_dispatch_detects_speckit_skills_layout to assert
the -p prompt contains /speckit-plan and the user args.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-04-23 08:26:59 -05:00
sha0dow
b278d66b2c docs(install): add pipx as alternative installation method (#2288)
* docs(install): add pipx as alternative installation method

- Add pipx commands to README.md installation section
- Add note about pipx compatibility to docs/installation.md
- Mention pipx persistent installation in docs/quickstart.md
- Add pipx upgrade instructions to docs/upgrade.md
- Clarify that project has no uv-specific dependencies

Refs: https://github.com/github/spec-kit/discussions/2255

* docs(install): address Copilot feedback - update prerequisites and upgrade references for pipx

* Update docs/quickstart.md

markdownlint’s MD012 (enabled in this repo) flags multiple consecutive blank lines

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update docs/upgrade.md

In the Quick Reference table, the label “pipx upgrade” is misleading because the command shown is `pipx install --force ...` (a reinstall). by copilot.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 08:12:17 -05:00
Dyan Galih
709457cec2 Add Memory MD community extension (#2327) 2026-04-23 07:50:26 -05:00
Kevin Brown
9e259e1f8d Update version-guard to v1.2.0 (#2321)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 16:44:06 -05:00
Manfred Riem
3970855797 fix: --force now overwrites shared infra files during init and upgrade (#2320)
* fix: --force now overwrites shared infra files during init and upgrade

_install_shared_infra() previously skipped all existing files under
.specify/scripts/ and .specify/templates/, regardless of --force.
This meant users could never receive upstream fixes to shared scripts
or templates after initial project setup.

Changes:
- Add force parameter to _install_shared_infra(); when True, existing
  files are overwritten with the latest bundled versions
- Wire force=True through specify init --here --force and
  specify integration upgrade --force call sites
- Replace hidden logging.warning with visible console output listing
  skipped files and suggesting --force
- Fix contradictory upgrade docs that claimed --force updated shared
  infra (it didn't) and warned about overwrites (they didn't happen)
- Add 6 tests: unit tests for skip/overwrite/warning behavior, plus
  end-to-end CLI tests for both --force and non-force paths

Fixes #2319

* fix: improve skip warning to suggest specific commands

Address review feedback: the generic '--force' suggestion was
misleading when _install_shared_infra is called from integration
install/switch (which don't have a --force for shared infra).
Now points users to the specific commands that can refresh shared
infra: 'specify init --here --force' or 'specify integration
upgrade --force'.
2026-04-22 16:40:41 -05:00
Manfred Riem
f612e1a30d chore: release 0.7.5, begin 0.7.6.dev0 development (#2322)
* chore: bump version to 0.7.5

* chore: begin 0.7.6.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-22 16:26:44 -05:00
swithek
ecb3b94b43 fix: resolve skill placeholders for all SKILL.md agents, not just codex/kimi (#2313)
* fix: resolve skill placeholders for all SKILL.md agents, not just codex/kimi

* chore: remove unused NATIVE_SKILLS_AGENTS constant
2026-04-22 15:12:44 -05:00
김준호
c5c20134df feat(cli): add specify self check and self upgrade stub (#2316)
* feat(cli): add specify self check and self upgrade stub (#2282)

Introduce a new `specify self` Typer sub-app with two subcommands.

`specify self check` performs a read-only lookup against the GitHub Releases
API, compares the installed version to the latest tag with PEP 440 semantics,
and prints one of four verdicts (newer-available, up-to-date, indeterminate,
graceful-failure). When a newer stable release is available, the output
includes a copy-pasteable `uv tool install --force --from git+...@<tag>`
reinstall command. `GH_TOKEN` / `GITHUB_TOKEN` is attached as a bearer
credential when set so users behind shared IPs escape the anonymous 60/hour
rate limit.

`specify self upgrade` is a documented non-destructive stub in this release:
three-line guidance output, exit 0, no outbound call, no install-method
detection. The real destructive implementation is planned as follow-up work.

Failure categorization is a fixed three-entry enum (offline or timeout,
rate limited, HTTP <code>). Anything outside those three categories
propagates as a non-zero exit so bugs surface instead of being silently
swallowed. No machine-readable output, no retries, no caching in this
release — see issue #2282 discussion.

Tests mock `urllib.request.urlopen`; the suite performs zero real network
calls. Full regression suite: 1586 passed.

* fix(cli): disable Rich highlight for deterministic output

Rich's default `highlight=True` applies ANSI color to detected patterns
(integers, version strings, paths) whenever stdout is deemed a TTY.
This caused intermittent failures in existing pytest assertions in
tests/test_cli_version.py and tests/test_extensions.py::TestExtensionRemoveCLI
that compare plain-text output without passing through `strip_ansi()`.

Setting `Console(highlight=False)` globally makes all CLI output
deterministic and fixes the flake without modifying the affected tests.
The numeric cyan highlighting was not a documented part of the CLI
visual contract.

* fix: address copilot review feedback

* fix: tighten self-check token handling

* fix: align self-check helpers and script metadata

* fix: harden self-check version handling

* fix: guard self-check failure rendering
2026-04-22 13:33:03 -05:00
Kevin Brown
58f7a43ec3 Update version-guard to v1.1.0 (#2318)
- Version: 1.0.0 → 1.1.0
- Commands: 1 → 3 (check, load, validate)
- Hooks: 2 → 4 (before_plan, before_tasks, before_implement, after_implement)
- Added: persistent constraints artifact, two-channel model, CVE detection,
  decision record fallback for greenfield projects, skip artifact persistence

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-22 13:27:56 -05:00
Manfred Riem
efb04e26eb docs: move community presets from README to docs/community (#2314)
Move the community presets table from the main README to a dedicated
docs/community/presets.md page, matching the pattern used for
walkthroughs and friends.

- Add docs/community/presets.md with the full presets table
- Add Claude AskUserQuestion preset (was in catalog but missing from table)
- Add Presets entry to docs/toc.yml under Community
- Replace inline README table with a short link to the docs page
2026-04-22 10:05:14 -05:00
TortoiseWolfe
c52ea23ba2 catalog: add wireframe extension (v0.1.1) (#2262)
* catalog: add wireframe extension

Adds https://github.com/TortoiseWolfe/spec-kit-extension-wireframe
(v0.1.0) to the community catalog. Provides a visual feedback loop
for spec-driven development: SVG wireframe generation, review, and
sign-off. Approved wireframes become spec constraints honored by
/plan, /tasks, and /implement.

Supersedes #1410 — the old PR predated the extension system
introduced in #2130 and proposed commands in templates/commands/,
which is no longer the right home for third-party commands.

* catalog: address review feedback (position + author)

Two changes per Copilot review:
- Move `wireframe` entry alphabetically between `whatif` and
  `worktree` (was appended after `worktrees`).
- Simplify `author` from "TortoiseWolfe (turtlewolfe.com)" to
  just "TortoiseWolfe" so the exact-match author filter in
  `ExtensionCatalog.search` finds the entry. Portfolio URL
  remains accessible via `homepage`/`repository`.

Thanks @Copilot, @mnriem for the review.

* docs(readme): add Wireframe Visual Feedback Loop row

Addresses @mnriem's follow-up: the README extension table also
needs an entry, not just the catalog JSON. Slots in alphabetically
between "What-if Analysis" and "Worktree Isolation" with category
`visibility` and Read+Write effect (since sign-off writes the
approved wireframe paths into spec.md).

* catalog: use speckit-prefixed command names in wireframe description

Address remaining Copilot review comment on PR #2262. The actual
commands are /speckit.plan, /speckit.tasks, /speckit.implement;
the unprefixed names would mislead catalog users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* catalog: bump wireframe extension to v0.1.1

v0.1.1 of spec-kit-extension-wireframe ships the /speckit.-prefixed
command references in extension.yml and README.md. This updates the
catalog entry to point at the new release tag so `specify extension
add wireframe` installs the corrected version.

* catalog: set wireframe created_at to current timestamp

Per EXTENSION-PUBLISHING-GUIDE.md: newly added entries should use
the current timestamp for both created_at and updated_at. The 04-17
value reflected when I drafted the entry locally, not when the
catalog submission landed.

---------

Co-authored-by: TortoiseWolfe <jonpohlner@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:20:13 -05:00
Manfred Riem
d402a392c3 Move community walkthroughs from README to docs/community (#2312)
* Move community walkthroughs from README to docs/community

Extract the community walkthroughs section from README.md into its own
docs/community/walkthroughs.md file and replace it with a short summary
linking to the GitHub Pages URL.

* Address review: fix double-See phrasing, add walkthroughs to docs nav
2026-04-22 09:17:48 -05:00
Ash Brener
deb80956f3 docs(readme): list red-team in community-extensions table (#2311)
Follow-up to #2306 (merged). Per maintainer request
(https://github.com/github/spec-kit/pull/2306#issuecomment-4296655643),
adds the red-team entry to the alphabetically-ordered community-extensions
table in README.md so the extension is discoverable alongside the other
community entries — not only via catalog.community.json.

Slotted alphabetically between "Reconcile Extension" and "Repository
Index". Category: docs. Effect: Read+Write (produces a structured
findings-report file at specs/<feature-id>/red-team-findings-*.md; does
not modify specs — every resolution is maintainer-authorised).

Co-authored-by: Ash Brener <ashley@midletearth.com>
2026-04-22 09:03:06 -05:00
Ash Brener
4dcf2921d1 feat(catalog): add red-team extension to community catalog (#2306)
* feat(catalog): add red-team extension

Adds the `red-team` community extension to the catalog:

- Adversarial review of functional specs before /speckit.plan locks in
  architecture.
- Complements /speckit.clarify (correctness) and /speckit.analyze
  (consistency) with parallel adversarial lens agents.
- One command: speckit.red-team.run
- MIT licensed; requires spec-kit >= 0.7.0.

Origin: this extension was originally proposed as a core command
(github/spec-kit#2303). Per maintainer guidance (mnriem's comment on
that PR), it's been restructured as a community extension hosted at
https://github.com/ashbrener/spec-kit-red-team.

Dogfood-validated on a 500-line functional spec: 5 lens agents
dispatched in parallel returned 25 findings in ~1.5 min wall-clock,
19 of which met the meaningful-finding bar (severity >= HIGH AND
novel adversarial angle that clarify/analyze structurally cannot
catch). Full detail in the extension's CHANGELOG.

* catalog: shorten red-team description to fit <200 char schema limit

Resolves Copilot review comment on #2306. Previous description (259
chars) exceeded the extensions/EXTENSION-PUBLISHING-GUIDE.md Appendix
schema ceiling. Shortened to 188 chars, keeping the distinctive
value proposition (adversarial, complements clarify/analyze) and
moving the per-phase mechanics to the extension's own README.

* catalog: bump red-team to v1.0.1 (lower required spec-kit version)

Follow-up to v1.0.0 catalog entry:
- version: 1.0.0 -> 1.0.1
- download_url: points at v1.0.1 release asset
- requires.speckit_version: >=0.7.0 -> >=0.1.0

The v1.0.0 requirement was too strict and blocked installation on
common 0.6.x field versions (confirmed via local install attempt).
The extension uses no 0.7.x-specific APIs; matches community norm
(reconcile, refine, others use >=0.1.0).

* catalog: bump red-team to v1.0.2 (adds mandatory before_plan gate)

v1.0.2 ships a /speckit.red-team.gate command wired as a mandatory
before_plan hook so /speckit.plan auto-invokes it on every run against
qualifying specs. Non-qualifying specs return PROCEED silently; qualifying
specs without findings on record return HALT with explicit remediation
(run /speckit.red-team.run, or opt out via --skip-red-team-gate: <reason>
which is recorded as an Accepted Risk [red-team-skipped] in the plan).

Catalog metadata delta:
- version: 1.0.1 -> 1.0.2
- download_url: v1.0.2/red-team-v1.0.2.zip
- provides.commands: 1 -> 2 (adds speckit.red-team.gate)
- provides.hooks: 0 -> 1 (adds before_plan hook)

No breaking changes. Projects that do not want the gate simply do not
install the extension.

---------

Co-authored-by: Ash Brener <ashley@midletearth.com>
2026-04-22 08:33:08 -05:00
WangX
dd9c0b0500 Add superpowers-bridge community extension (#2309)
* Add superpowers-bridge community extension

Adds the superpowers-bridge extension by WangX0111 to the community
catalog and README table. This extension bridges spec-kit with
obra/superpowers (brainstorming, TDD, subagent-driven-development,
code-review) into a unified, resumable workflow with graceful
degradation and session progress tracking.

Extension details:
- ID: superpowers-bridge
- Repository: https://github.com/WangX0111/superspec
- Version: 1.0.0
- Commands: 5, Hooks: 3
- License: MIT

* Address Copilot review feedback

- Update top-level updated_at to 2026-04-22
- Shorten description to under 200 characters

---------

Co-authored-by: 乘浩 <wch453799@alibaba-inc.com>
2026-04-22 08:14:58 -05:00
Kennedy
22e76995c7 feat: implement preset wrap strategy (#2189)
* feat: implement strategy: wrap

* fix: resolve merge conflict for strategy wrap correctness

* feat: multi-preset composable wrapping with priority ordering

Implements comment #4 from PR review: multiple installed wrap presets
now compose in priority order rather than overwriting each other.

Key changes:
- PresetResolver.resolve() gains skip_presets flag; resolve_core() wraps
  it to skip tier 2, preventing accidental nesting during replay
- _replay_wraps_for_command() recomposed all enabled wrap presets for a
  command in ascending priority order (innermost-first) after any
  install or remove
- _replay_skill_override() keeps SKILL.md in sync with the recomposed
  command body for ai-skills-enabled projects
- install_from_directory() detects strategy: wrap commands, stores
  wrap_commands in the registry entry, and calls replay after install
- remove() reads wrap_commands before deletion, removes registry entry
  before rmtree so replay sees post-removal state, then replays
  remaining wraps or unregisters when none remain

Tests: TestResolveCore (5), TestReplayWrapsForCommand (5),
TestInstallRemoveWrapLifecycle (5), plus 2 skill/alias regression tests

* fix: resolve extension commands via manifest file mapping

PresetResolver.resolve_extension_command_via_manifest() consults each
installed extension.yml to find the actual file declared for a command
name, rather than assuming the file is named <cmd_name>.md.  This fixes
_substitute_core_template for extensions like selftest where the manifest
maps speckit.selftest.extension → commands/selftest.md.

Resolution order in _substitute_core_template is now:
  1. resolve_core(cmd_name) — project overrides win, then name-based lookup
  2. resolve_extension_command_via_manifest(cmd_name) — manifest fallback
  3. resolve_core(short_name) — core template short-name fallback

Path traversal guard mirrors the containment check already present in
ExtensionManager to reject absolute paths or paths escaping the extension
root.

* fix: add bundled core_pack as Priority 5 in PresetResolver.resolve()

resolve_core() was returning None for built-in commands (implement,
specify, etc.) because PresetResolver only checked .specify/templates/
commands/ (Priority 4), which is never populated for commands in a
normal project. strategy:wrap presets rely on resolve_core() to fetch
the {CORE_TEMPLATE} body, so the wrap was silently skipped and SKILL.md
was never updated.

Priority 5 now checks core_pack/commands/ (wheel install) or
repo_root/templates/commands/ (source checkout), mirroring the pattern
used by _locate_core_pack() elsewhere.

Updated two tests whose assertions assumed resolve_core() always
returned None when .specify/templates/commands/ was absent.

* fix: harden preset wrap replay removal

* fix: stabilize existing directory error output

* fix: track outermost_pack_id from contributing preset; use Path.parts in tests

- outermost_pack_id now updates alongside outermost_frontmatter inside
  the wrap loop, so it reflects the actual last contributing preset
  rather than always taking wrap_presets[0] (which may have been skipped)
- Replace str(path) substring checks in TestResolveCore with Path.parts
  tuple comparisons for correct behaviour on Windows (CI runs windows-latest)

* fix: guard against non-mapping YAML manifests; apply integration post-processing in replay

- ExtensionManifest._load raises ValidationError for non-dict YAML roots instead of TypeError
- PresetManager._replay_wraps_for_command calls integration.post_process_skill_content,
  matching _register_skills behaviour
- PresetResolver skips extensions that raise OSError/TypeError/AttributeError on manifest load
- Tests: non-mapping YAML, OSError manifest skip, and replay integration post-processing
2026-04-21 15:02:31 -05:00
김준호
569d18a59d fix(agents): block directory traversal in command write paths (#2229) (#2296)
Extend the alias containment guard from b67b285 to the two remaining
write paths that derive filenames from free-form command/alias names:

- Primary command write in CommandRegistrar.register_commands()
- CommandRegistrar.write_copilot_prompt()

Consolidate the check into a shared _ensure_inside() helper. Per
maintainer guidance on #2229, use a lexical
(os.path.normpath + Path.is_relative_to) containment check rather than
resolve() so `..` / absolute-path traversal is rejected while
intentionally symlinked sub-directories under an agent's commands
directory (e.g. .claude/skills/shared -> /team/shared-skills) keep
working for existing extension setups.

Add 22 parametrised regression cases covering traversal payloads on
primary commands, aliases, and the Copilot companion prompt, plus a
positive case that confirms symlinked sub-directories remain supported.
2026-04-21 12:06:09 -05:00
Manfred Riem
f10fd07481 chore: release 0.7.4, begin 0.7.5.dev0 development (#2299)
* chore: bump version to 0.7.4

* chore: begin 0.7.5.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-21 11:50:23 -05:00
118 changed files with 14166 additions and 1254 deletions

5
.github/CODEOWNERS vendored
View File

@@ -1,3 +1,8 @@
# Global code owner
* @mnriem
# Community catalog files — explicit ownership for when global ownership expands
/extensions/catalog.community.json @mnriem
/integrations/catalog.community.json @mnriem
/presets/catalog.community.json @mnriem

View File

@@ -8,7 +8,7 @@ body:
value: |
Thanks for requesting a new agent! Before submitting, please check if the agent is already supported.
**Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, Tabnine CLI, Antigravity, IBM Bob, Mistral Vibe, Kimi Code, Trae, Pi Coding Agent, iFlow CLI
**Currently supported agents**: Claude Code, Gemini CLI, GitHub Copilot, Cursor, Qwen Code, opencode, Codex CLI, Windsurf, Kilo Code, Auggie CLI, Roo Code, CodeBuddy, Qoder CLI, Kiro CLI, Amp, SHAI, Tabnine CLI, Antigravity, IBM Bob, Mistral Vibe, Kimi Code, Trae, Pi Coding Agent, iFlow CLI, Devin for Terminal
- type: input
id: agent-name

View File

@@ -95,11 +95,18 @@ body:
validations:
required: true
- type: input
id: required-extensions
attributes:
label: Required Extensions (optional)
description: Comma-separated list of required extension IDs (e.g., aide)
placeholder: "e.g., aide, canon"
- type: textarea
id: templates-provided
attributes:
label: Templates Provided
description: List the template overrides your preset provides
description: List the template overrides your preset provides (enter "None" if command-only)
placeholder: |
- spec-template.md — adds compliance section
- plan-template.md — includes audit checkpoints
@@ -110,10 +117,19 @@ body:
- type: textarea
id: commands-provided
attributes:
label: Commands Provided (optional)
description: List any command overrides your preset provides
label: Commands Provided
description: List the command overrides your preset provides (enter "None" if template-only)
placeholder: |
- speckit.specify.md — customized for compliance workflows
validations:
required: true
- type: input
id: scripts-count
attributes:
label: Number of Scripts (optional)
description: How many scripts does your preset provide? (leave empty if none)
placeholder: "e.g., 1"
- type: textarea
id: tags

59
.github/workflows/catalog-assign.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: "Catalog: Auto-assign submission"
on:
issues:
types: [opened, labeled]
jobs:
assign:
if: >
(github.event.action == 'opened' && (
contains(github.event.issue.labels.*.name, 'extension-submission') ||
contains(github.event.issue.labels.*.name, 'preset-submission')
)) ||
(github.event.action == 'labeled' && (
github.event.label.name == 'extension-submission' ||
github.event.label.name == 'preset-submission'
))
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/github-script@v7
with:
script: |
const issue = context.payload.issue;
const assigned = (issue.assignees || []).map(a => a.login);
const marker = '<!-- catalog-assign-bot -->';
// Assign mnriem if not already assigned
if (!assigned.includes('mnriem')) {
try {
await github.rest.issues.addAssignees({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
assignees: ['mnriem'],
});
} catch (e) {
console.log(`Warning: could not assign mnriem: ${e.message}`);
}
}
// Post team notification if not already posted
const comments = await github.paginate(
github.rest.issues.listComments,
{
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
}
);
if (!comments.some(c => c.body && c.body.includes(marker))) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: marker + '\ncc @github/spec-kit-maintainers — new catalog submission for review.',
});
}

View File

@@ -19,14 +19,14 @@ jobs:
language: [ 'actions', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
with:
category: "/language:${{ matrix.language }}"

View File

@@ -30,12 +30,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0 # Fetch all history for git info
- name: Setup .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4
with:
dotnet-version: '8.x'
@@ -48,10 +48,10 @@ jobs:
docfx docfx.json
- name: Setup Pages
uses: actions/configure-pages@v6
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6
- name: Upload artifact
uses: actions/upload-pages-artifact@v5
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
with:
path: 'docs/_site'
@@ -66,5 +66,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5

View File

@@ -12,10 +12,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Run markdownlint-cli2
uses: DavidAnson/markdownlint-cli2-action@ce4853d43830c74c1753b39f3cf40f71c2031eb9 # v23
uses: DavidAnson/markdownlint-cli2-action@6b51ade7a9e4a75a7ad929842dd298a3804ebe8b # v23
with:
globs: |
'**/*.md'

View File

@@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}

View File

@@ -12,7 +12,7 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -86,4 +86,3 @@ jobs:
--notes-file release_notes.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -14,7 +14,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
- uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with:
# Days of inactivity before an issue or PR becomes stale
days-before-stale: 150

View File

@@ -13,13 +13,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Set up Python
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
@@ -34,13 +34,13 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Install uv
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ matrix.python-version }}

101
AGENTS.md
View File

@@ -20,23 +20,17 @@ src/specify_cli/integrations/
├── base.py # IntegrationBase, MarkdownIntegration, TomlIntegration, YamlIntegration, SkillsIntegration
├── manifest.py # IntegrationManifest (file tracking)
├── claude/ # Example: SkillsIntegration subclass
── __init__.py # ClaudeIntegration class
│ └── scripts/ # Thin wrapper scripts
│ ├── update-context.sh
│ └── update-context.ps1
── __init__.py # ClaudeIntegration class
├── gemini/ # Example: TomlIntegration subclass
── __init__.py
│ └── scripts/
── __init__.py
├── windsurf/ # Example: MarkdownIntegration subclass
── __init__.py
│ └── scripts/
── __init__.py
├── copilot/ # Example: IntegrationBase subclass (custom setup)
── __init__.py
│ └── scripts/
── __init__.py
└── ... # One subpackage per supported agent
```
The registry is the **single source of truth for Python integration metadata**. Supported agents, their directories, formats, and capabilities are derived from the integration classes for the Python integration layer. However, context-update behavior still requires explicit cases in the shared dispatcher scripts (`scripts/bash/update-agent-context.sh` and `scripts/powershell/update-agent-context.ps1`), which currently maintain their own supported-agent lists and agent-key→context-file mappings until they are migrated to registry-based dispatch.
The registry is the **single source of truth for Python integration metadata**. Supported agents, their directories, formats, capabilities, and context files are derived from the integration classes for the Python integration layer.
---
@@ -179,63 +173,11 @@ def _register_builtins() -> None:
# ...
```
### 4. Add scripts
### 4. Context file behavior
Create two thin wrapper scripts in `src/specify_cli/integrations/<package_dir>/scripts/` that delegate to the shared context-update scripts. Each is ~25 lines of boilerplate.
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.
> **Note on `<package_dir>` vs `<key>`:** `<package_dir>` is the Python-safe directory name for your integration — it matches `<key>` exactly when the key contains no hyphens (e.g., key `"gemini"` → `gemini/`), but uses underscores when it does (e.g., key `"kiro-cli"` → `kiro_cli/`). The `IntegrationBase.key` class attribute always retains the original hyphenated value (e.g., `key = "kiro-cli"`), since that is what the CLI and registry use.
**`update-context.sh`:**
```bash
#!/usr/bin/env bash
# update-context.sh — <Agent Name> integration: create/update <context_file>
set -euo pipefail
_script_dir="$(cd "$(dirname "$0")" && pwd)"
_root="$_script_dir"
while [ "$_root" != "/" ] && [ ! -d "$_root/.specify" ]; do _root="$(dirname "$_root")"; done
if [ -z "${REPO_ROOT:-}" ]; then
if [ -d "$_root/.specify" ]; then
REPO_ROOT="$_root"
else
git_root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
if [ -n "$git_root" ] && [ -d "$git_root/.specify" ]; then
REPO_ROOT="$git_root"
else
REPO_ROOT="$_root"
fi
fi
fi
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" <key>
```
**`update-context.ps1`:**
```powershell
# update-context.ps1 — <Agent Name> integration: create/update <context_file>
$ErrorActionPreference = 'Stop'
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition
$repoRoot = try { git rev-parse --show-toplevel 2>$null } catch { $null }
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = $scriptDir
$fsRoot = [System.IO.Path]::GetPathRoot($repoRoot)
while ($repoRoot -and $repoRoot -ne $fsRoot -and -not (Test-Path (Join-Path $repoRoot '.specify'))) {
$repoRoot = Split-Path -Parent $repoRoot
}
}
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType <key>
```
Replace `<key>` with your integration key and `<Agent Name>` / `<context_file>` with the appropriate values.
You must also add the agent to the shared context-update scripts so the shared dispatcher recognises the new key:
- **`scripts/bash/update-agent-context.sh`** — add a file-path variable and a case in `update_specific_agent()`.
- **`scripts/powershell/update-agent-context.ps1`** — add a file-path variable, add the new key to the `AgentType` parameter's `[ValidateSet(...)]`, add a switch case in `Update-SpecificAgent`, and add an entry in `Update-AllExistingAgents`.
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
@@ -264,13 +206,13 @@ The base classes handle most work automatically. Override only when the agent de
| Override | When to use | Example |
|---|---|---|
| `command_filename(template_name)` | Custom file naming or extension | Copilot → `speckit.{name}.agent.md` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` |
| `options()` | Integration-specific CLI flags via `--integration-options` | Codex → `--skills` flag, Copilot → `--skills` flag |
| `setup()` | Custom install logic (companion files, settings merge) | Copilot → `.agent.md` + `.prompt.md` + `.vscode/settings.json` (default) or `speckit-<name>/SKILL.md` (skills mode) |
| `teardown()` | Custom uninstall logic | Rarely needed; base handles manifest-tracked files |
**Example — Copilot (fully custom `setup`):**
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
Copilot extends `IntegrationBase` directly because it creates `.agent.md` commands, companion `.prompt.md` files, and merges `.vscode/settings.json`. It also supports a `--skills` mode that scaffolds `speckit-<name>/SKILL.md` under `.github/skills/` using composition with an internal `_CopilotSkillsHelper`. See `src/specify_cli/integrations/copilot/__init__.py` for the full implementation.
### 7. Update Devcontainer files (Optional)
@@ -391,6 +333,24 @@ Implementation: Extends `IntegrationBase` with custom `setup()` method that:
2. Generates companion `.prompt.md` files
3. Merges VS Code settings
**Skills mode (`--skills`):** Copilot also supports an alternative skills-based layout
via `--integration-options="--skills"`. When enabled:
- Commands are scaffolded as `speckit-<name>/SKILL.md` under `.github/skills/`
- No companion `.prompt.md` files are generated
- No `.vscode/settings.json` merge
- `post_process_skill_content()` injects a `mode: speckit.<stem>` frontmatter field
- `build_command_invocation()` returns `/speckit-<stem>` instead of bare args
The two modes are mutually exclusive — a project uses one or the other:
```bash
# Default mode: .agent.md agents + .prompt.md companions + settings merge
specify init my-project --integration copilot
# Skills mode: speckit-<name>/SKILL.md under .github/skills/
specify init my-project --integration copilot --integration-options="--skills"
```
### Forge Integration
Forge has special frontmatter and argument requirements:
@@ -404,7 +364,6 @@ Implementation: Extends `MarkdownIntegration` with custom `setup()` method that:
3. Applies Forge-specific transformations via `_apply_forge_transformations()`
4. Strips `handoffs` frontmatter key
5. Injects missing `name` fields
6. Ensures the shared `update-agent-context.*` scripts include a `forge` case that maps context updates to `AGENTS.md` and lists `forge` in their usage/help text
### Goose Integration
@@ -418,7 +377,7 @@ Implementation: Extends `YamlIntegration` (parallel to `TomlIntegration`):
2. Extracts title and description from frontmatter
3. Renders output as Goose recipe YAML (version, title, description, author, extensions, activities, prompt)
4. Uses `yaml.safe_dump()` for header fields to ensure proper escaping
5. Context updates map to `AGENTS.md` (shared with opencode/codex/pi/forge)
5. Sets `context_file = "AGENTS.md"` so the base setup manages the Spec Kit context section there
## Common Pitfalls

View File

@@ -2,6 +2,126 @@
<!-- insert new changelog below this comment -->
## [0.8.6] - 2026-05-06
### Changed
- Load constitution context in `/speckit.implement` to enforce governance during implementation (#2460)
- feat: improve catalog submission templates and CODEOWNERS (#2401)
- fix: validate URL scheme in build_github_request (#2449)
- Add Architecture Guard to community catalog (#2430)
- Add multi-model-review extension to community catalog (#2446)
- Update Ralph Loop to v1.0.2 (#2435)
- Pin GitHub Actions by SHA (#2441)
- fix(workflows): require project for catalog list (#2436)
- Add agent-parity-governance to community catalog (#2382)
- chore: release 0.8.5, begin 0.8.6.dev0 development (#2447)
## [0.8.5] - 2026-05-04
### Changed
- feat(presets): add Spec2Cloud preset for Azure deployment workflow (#2413)
- update security-review and memory-md extensions to latest versions (#2445)
- fix: honor template overrides for tasks-template (#2278) (#2292)
- Add token-analyzer to community catalog (#2433)
- docs: add April 2026 newsletter (#2434)
- feat: emit init-time notice for git extension default change (#2165) (#2432)
- Update DyanGalih(Memory Hub and Security Review) community extensions (#2429)
- Support controlled multi-install for safe AI agent integrations (#2389)
- chore(integrations): clean up docs and project guard (#2428)
- chore: release 0.8.4, begin 0.8.5.dev0 development (#2431)
## [0.8.4] - 2026-05-01
### Changed
- fix(specify): correct self-referencing step number in validation flow (#2152)
- chore(deps): bump DavidAnson/markdownlint-cli2-action (#2425)
- Add security-governance to community catalog (#2386)
- Add cross-platform-governance to community catalog (#2384)
- Add architecture-governance to community catalog (#2383)
- Add a11y-governance to community catalog (#2381)
- feat(extensions): add Spec2Cloud extension for Azure deployment workflow (#2412)
- fix: migrate extension commands on integration switch (#2404)
- feat: add Squad Bridge extension to community catalog (#2417)
- chore: release 0.8.3, begin 0.8.4.dev0 development (#2418)
## [0.8.3] - 2026-04-29
### Changed
- Add Work IQ extension to community catalog (#2415)
- feat(integrations): add Devin for Terminal skills-based integration (#2364)
- fix: include --from git+... in upgrade hint to avoid PyPI squat package (#2411)
- fix: dispatch opencode commands via run (#2410)
- feat: add catalog discovery CLI commands (#2360)
- update security review extension catalog to v1.3.0 (#2374)
- chore(catalog): bump v-model extension to v0.6.0 (#2399)
- feat: add threatmodel extension to community catalog (#2369)
- Add isaqb-architecture-governance to community catalog (#2385)
- chore: release 0.8.2, begin 0.8.3.dev0 development (#2397)
## [0.8.2] - 2026-04-28
### Changed
- Add MarkItDown Document Converter extension to community catalog (#2390)
- feat: Speckit preset fiction book v1.7 - Support for RAG (Chroma DB) offline semantic search (#2367)
- fix(extensions): use explicit UTF-8 encoding when reading manifest YAML (#2370)
- catalog: add m365 community extension
- docs: replace deprecated --ai flag with --integration in all documentation (#2359)
- feat(extensions,presets): authenticate GitHub-hosted catalog and download requests with GITHUB_TOKEN/GH_TOKEN (#2331)
- Update extensify to v1.1.0 in community catalog (#2337)
- feat(init): deprecate --no-git flag, gate deprecations at v0.10.0 (#2357)
- Add Spec Orchestrator extension to community catalog (#2350)
- chore: release 0.8.1, begin 0.8.2.dev0 development (#2356)
## [0.8.1] - 2026-04-24
### Changed
- fix(plan): use .specify/feature.json to allow /speckit.plan on custom git branches (#2305) (#2349)
- feat(vibe): migrate to SkillsIntegration from the old prompts-based MarkdownIntegration (#2336)
- docs: move community presets table to docs site, add missing entries (#2341)
- docs(presets): add lean preset README and enrich catalog metadata (#2340)
- fix: resolve command references per integration type (dot vs hyphen) (#2354)
- Update product-forge to v1.5.1 in community catalog (#2352)
- chore(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 (#2345)
- fix: replace xargs trim with sed to handle quotes in descriptions (#2351)
- feat: register jira preset in community catalog (#2224)
- feat: Preset screenwriting (#2332)
- chore: release 0.8.0, begin 0.8.1.dev0 development (#2333)
## [0.8.0] - 2026-04-23
### Changed
- feat(presets): Composition strategies (prepend, append, wrap) for templates, commands, and scripts (#2133)
- feat(copilot): support `--integration-options="--skills"` for skills-based scaffolding (#2324)
- docs(install): add pipx as alternative installation method (#2288)
- Add Memory MD community extension (#2327)
- Update version-guard to v1.2.0 (#2321)
- fix: `--force` now overwrites shared infra files during init and upgrade (#2320)
- chore: release 0.7.5, begin 0.7.6.dev0 development (#2322)
## [0.7.5] - 2026-04-22
### Changed
- fix: resolve skill placeholders for all SKILL.md agents, not just codex/kimi (#2313)
- feat(cli): add specify self check and self upgrade stub (#2316)
- Update version-guard to v1.1.0 (#2318)
- docs: move community presets from README to docs/community (#2314)
- catalog: add wireframe extension (v0.1.1) (#2262)
- Move community walkthroughs from README to docs/community (#2312)
- docs(readme): list red-team in community-extensions table (#2311)
- feat(catalog): add red-team extension to community catalog (#2306)
- Add superpowers-bridge community extension (#2309)
- feat: implement preset wrap strategy (#2189)
- fix(agents): block directory traversal in command write paths (#2229) (#2296)
- chore: release 0.7.4, begin 0.7.5.dev0 development (#2299)
## [0.7.4] - 2026-04-21
### Changed

View File

@@ -94,7 +94,7 @@ uv pip install -e .
# Ensure the `specify` binary in this environment points at your working tree so the agent runs the branch you're testing.
# Initialize a test project using your local changes
uv run specify init <temp-dir>/speckit-test --ai <agent> --offline
uv run specify init <temp-dir>/speckit-test --integration <agent>
cd <temp-dir>/speckit-test
# Open in your agent
@@ -102,7 +102,7 @@ cd <temp-dir>/speckit-test
#### Manual testing process
Any change that affects a slash command's behavior requires manually testing that command through an AI agent and submitting results with the PR.
Any change that affects a slash command's behavior requires manually testing that command through a coding agent and submitting results with the PR.
1. **Identify affected commands** — use the [prompt below](#determining-which-tests-to-run) to have your agent analyze your changed files and determine which commands need testing.
2. **Set up a test project** — scaffold from your local branch (see [Testing setup](#testing-setup)).

0
EOF Normal file
View File

109
README.md
View File

@@ -62,6 +62,10 @@ uv tool install specify-cli --from git+https://github.com/github/spec-kit.git@vX
# Or install latest from main (may include unreleased changes)
uv tool install specify-cli --from git+https://github.com/github/spec-kit.git
# Alternative: using pipx (also works)
pipx install git+https://github.com/github/spec-kit.git@vX.Y.Z
pipx install git+https://github.com/github/spec-kit.git
```
Then verify the correct version is installed:
@@ -77,9 +81,9 @@ And use the tool directly:
specify init <PROJECT_NAME>
# Or initialize in existing project
specify init . --ai copilot
specify init . --integration copilot
# or
specify init --here --ai copilot
specify init --here --integration copilot
# Check installed tools
specify check
@@ -89,6 +93,7 @@ To upgrade Specify, see the [Upgrade Guide](./docs/upgrade.md) for detailed inst
```bash
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z
# pipx users: pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z
```
#### Option 2: One-time Usage
@@ -100,9 +105,9 @@ Run directly without installing:
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <PROJECT_NAME>
# Or initialize in existing project
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init . --ai copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init . --integration copilot
# or
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --ai copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --integration copilot
```
**Benefits of persistent installation:**
@@ -118,7 +123,7 @@ If your environment blocks access to PyPI or GitHub, see the [Enterprise / Air-G
### 2. Establish project principles
Launch your AI assistant in the project directory. Most agents expose spec-kit as `/speckit.*` slash commands; Codex CLI in skills mode uses `$speckit-*` instead.
Launch your coding agent in the project directory. Most agents expose spec-kit as `/speckit.*` slash commands; Codex CLI in skills mode uses `$speckit-*` instead.
Use the **`/speckit.constitution`** command to create your project's governing principles and development guidelines that will guide all subsequent development.
@@ -169,7 +174,7 @@ Want to see Spec Kit in action? Watch our [video overview](https://www.youtube.c
## 🧩 Community Extensions
> [!NOTE]
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
> Community extensions are independently created and maintained by their respective authors. Maintainers only verify that catalog entries are complete and correctly formatted — they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
🔍 **Browse and search community extensions on the [Community Extensions website](https://speckit-community.github.io/extensions/).**
@@ -193,6 +198,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Agent Assign | Assign specialized Claude Code agents to spec-kit tasks for targeted execution | `process` | Read+Write | [spec-kit-agent-assign](https://github.com/xymelon/spec-kit-agent-assign) |
| AI-Driven Engineering (AIDE) | A structured 7-step workflow for building new projects from scratch with AI assistants — from vision through implementation | `process` | Read+Write | [aide](https://github.com/mnriem/spec-kit-extensions/tree/main/aide) |
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
| Architecture Guard | Continuous architecture governance for AI-assisted development. Reviews specs, plans, and code for architecture drift, producing structured refactor tasks and evolution proposals. | `process` | Read+Write | [spec-kit-architecture-guard](https://github.com/DyanGalih/spec-kit-architecture-guard) |
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
@@ -223,45 +229,58 @@ The following community-contributed extensions are available in [`catalog.commun
| MAQA Jira Integration | Jira integration for MAQA — syncs Stories and Subtasks as features progress through the board | `integration` | Read+Write | [spec-kit-maqa-jira](https://github.com/GenieRobot/spec-kit-maqa-jira) |
| MAQA Linear Integration | Linear integration for MAQA — syncs issues and sub-issues across workflow states as features progress | `integration` | Read+Write | [spec-kit-maqa-linear](https://github.com/GenieRobot/spec-kit-maqa-linear) |
| MAQA Trello Integration | Trello board integration for MAQA — populates board from specs, moves cards, real-time checklist ticking | `integration` | Read+Write | [spec-kit-maqa-trello](https://github.com/GenieRobot/spec-kit-maqa-trello) |
| MarkItDown Document Converter | Convert documents (PDF, Word, PowerPoint, Excel, and more) to Markdown for use as spec reference material | `docs` | Read+Write | [spec-kit-markitdown](https://github.com/BenBtg/spec-kit-markitdown) |
| Memory Loader | Loads .specify/memory/ files before lifecycle commands so LLM agents have project governance context | `docs` | Read-only | [spec-kit-memory-loader](https://github.com/KevinBrown5280/spec-kit-memory-loader) |
| Memory MD | Spec Kit extension for repository-native Markdown memory that captures durable decisions, bugs, and project context | `docs` | Read+Write | [spec-kit-memory-hub](https://github.com/DyanGalih/spec-kit-memory-hub) |
| 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) |
| 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) |
| OWASP LLM Threat Model | OWASP Top 10 for LLM Applications 2025 threat analysis on agent artifacts | `code` | Read-only | [spec-kit-threatmodel](https://github.com/NaviaSamal/spec-kit-threatmodel) |
| Plan Review Gate | Require spec.md and plan.md to be merged via MR/PR before allowing task generation | `process` | Read-only | [spec-kit-plan-review-gate](https://github.com/luno/spec-kit-plan-review-gate) |
| PR Bridge | Auto-generate pull request descriptions, checklists, and summaries from spec artifacts | `process` | Read-only | [spec-kit-pr-bridge-](https://github.com/Quratulain-bilal/spec-kit-pr-bridge-) |
| Presetify | Create and validate presets and preset catalogs | `process` | Read+Write | [presetify](https://github.com/mnriem/spec-kit-extensions/tree/main/presetify) |
| Product Forge | Full product lifecycle: research → product spec → SpecKit → implement → verify → test | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
| Product Forge | Full product lifecycle from research to release — portfolio, lite mode, monorepo, optional V-Model | `process` | Read+Write | [speckit-product-forge](https://github.com/VaiYav/speckit-product-forge) |
| Project Health Check | Diagnose a Spec Kit project and report health issues across structure, agents, features, scripts, extensions, and git | `visibility` | Read-only | [spec-kit-doctor](https://github.com/KhawarHabibKhan/spec-kit-doctor) |
| Project Status | Show current SDD workflow progress — active feature, artifact status, task completion, workflow phase, and extensions summary | `visibility` | Read-only | [spec-kit-status](https://github.com/KhawarHabibKhan/spec-kit-status) |
| QA Testing Extension | Systematic QA testing with browser-driven or CLI-based validation of acceptance criteria from spec | `code` | Read-only | [spec-kit-qa](https://github.com/arunt14/spec-kit-qa) |
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss/spec-kit-ralph) |
| Ralph Loop | Autonomous implementation loop using AI agent CLI | `code` | Read+Write | [spec-kit-ralph](https://github.com/Rubiss-Projects/spec-kit-ralph) |
| Reconcile Extension | Reconcile implementation drift by surgically updating feature artifacts. | `docs` | Read+Write | [spec-kit-reconcile](https://github.com/stn1slv/spec-kit-reconcile) |
| Red Team | Adversarial review of specs before /speckit.plan — parallel lens agents surface risks that clarify/analyze structurally can't (prompt injection, integrity gaps, cross-spec drift, silent failures). Produces a structured findings report; no auto-edits to specs. | `docs` | Read+Write | [spec-kit-red-team](https://github.com/ashbrener/spec-kit-red-team) |
| Repository Index | Generate index for existing repo for overview, architecture and module level. | `docs` | Read-only | [spec-kit-repoindex](https://github.com/liuyiyu/spec-kit-repoindex) |
| Retro Extension | Sprint retrospective analysis with metrics, spec accuracy assessment, and improvement suggestions | `process` | Read+Write | [spec-kit-retro](https://github.com/arunt14/spec-kit-retro) |
| Retrospective Extension | Post-implementation retrospective with spec adherence scoring, drift analysis, and human-gated spec updates | `docs` | Read+Write | [spec-kit-retrospective](https://github.com/emi-dm/spec-kit-retrospective) |
| Review Extension | Post-implementation comprehensive code review with specialized agents for code quality, comments, tests, error handling, type design, and simplification | `code` | Read-only | [spec-kit-review](https://github.com/ismaelJimenez/spec-kit-review) |
| Ripple | Detect side effects that tests can't catch after implementation — delta-anchored analysis across 9 domain-agnostic categories | `code` | Read+Write | [spec-kit-ripple](https://github.com/chordpli/spec-kit-ripple) |
| SDD Utilities | Resume interrupted workflows, validate project health, and verify spec-to-task traceability | `process` | Read+Write | [speckit-utils](https://github.com/mvanhorn/speckit-utils) |
| Security Review | Comprehensive security audit of codebases using AI-powered DevSecOps analysis | `code` | Read-only | [spec-kit-security-review](https://github.com/DyanGalih/spec-kit-security-review) |
| Security Review | Full-project secure-by-design security audits plus staged, branch/PR, plan, task, follow-up, and apply reviews | `code` | Read+Write | [spec-kit-security-review](https://github.com/DyanGalih/spec-kit-security-review) |
| SFSpeckit | Enterprise Salesforce SDLC with 18 commands for the full SDD lifecycle. | `process` | Read+Write | [spec-kit-sf](https://github.com/ysumanth06/spec-kit-sf) |
| Ship Release Extension | Automates release pipeline: pre-flight checks, branch sync, changelog generation, CI verification, and PR creation | `process` | Read+Write | [spec-kit-ship](https://github.com/arunt14/spec-kit-ship) |
| Spec Reference Loader | Reads the ## References section from the feature spec and loads only the listed docs into context | `docs` | Read-only | [spec-kit-spec-reference-loader](https://github.com/KevinBrown5280/spec-kit-spec-reference-loader) |
| Spec Critique Extension | Dual-lens critical review of spec and plan from product strategy and engineering risk perspectives | `docs` | Read-only | [spec-kit-critique](https://github.com/arunt14/spec-kit-critique) |
| Spec Diagram | Auto-generate Mermaid diagrams of SDD workflow state, feature progress, and task dependencies | `visibility` | Read-only | [spec-kit-diagram-](https://github.com/Quratulain-bilal/spec-kit-diagram-) |
| Spec Orchestrator | Cross-feature orchestration — track state, select tasks, and detect conflicts across parallel specs | `process` | Read-only | [spec-kit-orchestrator](https://github.com/Quratulain-bilal/spec-kit-orchestrator) |
| Spec Refine | Update specs in-place, propagate changes to plan and tasks, and diff impact across artifacts | `process` | Read+Write | [spec-kit-refine](https://github.com/Quratulain-bilal/spec-kit-refine) |
| Spec Scope | Effort estimation and scope tracking — estimate work, detect creep, and budget time per phase | `process` | Read-only | [spec-kit-scope-](https://github.com/Quratulain-bilal/spec-kit-scope-) |
| Spec Sync | Detect and resolve drift between specs and implementation. AI-assisted resolution with human approval | `docs` | Read+Write | [spec-kit-sync](https://github.com/bgervin/spec-kit-sync) |
| Spec Validate | Comprehension validation, review gating, and approval state for spec-kit artifacts — staged quizzes, peer review SLA, and a hard gate before /speckit.implement | `process` | Read+Write | [spec-kit-spec-validate](https://github.com/aeltayeb/spec-kit-spec-validate) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure | `process` | Read+Write | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| SpecTest | Auto-generate test scaffolds from spec criteria, map coverage, and find untested requirements | `code` | Read+Write | [spec-kit-spectest](https://github.com/Quratulain-bilal/spec-kit-spectest) |
| Squad Bridge | Bootstrap and synchronize a Squad agent team from your Speckit spec and tasks | `process` | Read+Write | [spec-kit-squad](https://github.com/jwill824/spec-kit-squad) |
| Staff Review Extension | Staff-engineer-level code review that validates implementation against spec, checks security, performance, and test coverage | `code` | Read-only | [spec-kit-staff-review](https://github.com/arunt14/spec-kit-staff-review) |
| Status Report | Project status, feature progress, and next-action recommendations for spec-driven workflows | `visibility` | Read-only | [Open-Agent-Tools/spec-kit-status](https://github.com/Open-Agent-Tools/spec-kit-status) |
| Superpowers Bridge | Orchestrates obra/superpowers skills within the spec-kit SDD workflow across the full lifecycle (clarification, TDD, review, verification, critique, debugging, branch completion) | `process` | Read+Write | [superpowers-bridge](https://github.com/RbBtSn0w/spec-kit-extensions/tree/main/superpowers-bridge) |
| Superpowers Bridge (WangX0111) | Bridges spec-kit with obra/superpowers (brainstorming, TDD, subagent, code-review) into a unified, resumable workflow with graceful degradation and session progress tracking | `process` | Read+Write | [superspec](https://github.com/WangX0111/superspec) |
| TinySpec | Lightweight single-file workflow for small tasks — skip the heavy multi-step SDD process | `process` | Read+Write | [spec-kit-tinyspec](https://github.com/Quratulain-bilal/spec-kit-tinyspec) |
| Token Consumption Analyzer | Captures, analyzes, and compares token consumption across SDD workflows | `visibility` | Read-only | [spec-kit-token-analyzer](https://github.com/coderandhiker/spec-kit-token-analyzer) |
| V-Model Extension Pack | Enforces V-Model paired generation of development specs and test specs with full traceability | `docs` | Read+Write | [spec-kit-v-model](https://github.com/leocamello/spec-kit-v-model) |
| Verify Extension | Post-implementation quality gate that validates implemented code against specification artifacts | `code` | Read-only | [spec-kit-verify](https://github.com/ismaelJimenez/spec-kit-verify) |
| Verify Tasks Extension | Detect phantom completions: tasks marked [X] in tasks.md with no real implementation | `code` | Read-only | [spec-kit-verify-tasks](https://github.com/datastone-inc/spec-kit-verify-tasks) |
| Version Guard | Verify tech stack versions against live npm registries before planning and implementation | `process` | Read-only | [spec-kit-version-guard](https://github.com/KevinBrown5280/spec-kit-version-guard) |
| What-if Analysis | Preview the downstream impact (complexity, effort, tasks, risks) of requirement changes before committing to them | `visibility` | Read-only | [spec-kit-whatif](https://github.com/DevAbdullah90/spec-kit-whatif) |
| Wireframe Visual Feedback Loop | SVG wireframe generation, review, and sign-off for spec-driven development. Approved wireframes become spec constraints honored by /speckit.plan, /speckit.tasks, and /speckit.implement | `visibility` | Read+Write | [spec-kit-extension-wireframe](https://github.com/TortoiseWolfe/spec-kit-extension-wireframe) |
| Work IQ | Integrate Microsoft 365 organizational knowledge into spec-driven development workflows | `integration` | Read-only | [spec-kit-workiq](https://github.com/sakitA/spec-kit-workiq) |
| Worktree Isolation | Spawn isolated git worktrees for parallel feature development without checkout switching | `process` | Read+Write | [spec-kit-worktree](https://github.com/Quratulain-bilal/spec-kit-worktree) |
| Worktrees | Default-on worktree isolation for parallel agents — sibling or nested layout | `process` | Read+Write | [spec-kit-worktree-parallel](https://github.com/dango85/spec-kit-worktree-parallel) |
@@ -269,44 +288,16 @@ To submit your own extension, see the [Extension Publishing Guide](extensions/EX
## 🎨 Community Presets
Community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. See the full list on the [Community Presets](https://github.github.io/spec-kit/community/presets.html) page.
> [!NOTE]
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
> Community presets are third-party contributions and are not maintained by the Spec Kit team. Review them carefully before use, and see the docs page above for the full disclaimer.
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](presets/catalog.community.json):
| Preset | Purpose | Provides | Requires | URL |
|--------|---------|----------|----------|-----|
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. | 22 templates, 27 commands | — | [spec-kit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
To build and publish your own preset, see the [Presets Publishing Guide](presets/PUBLISHING.md).
To submit your own preset, see the [Presets Publishing Guide](presets/PUBLISHING.md).
## 🚶 Community Walkthroughs
> [!NOTE]
> Community walkthroughs are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their content before following along and use at your own discretion.
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
- **[Brownfield Go / React dashboard demo](https://github.com/mnriem/spec-kit-go-brownfield-demo)** — Demonstrates spec-kit driven entirely from the **terminal using GitHub Copilot CLI**. Extends NASA's open-source Hermes ground support system (Go) with a lightweight React-based web telemetry dashboard, showing that the full constitution → specify → plan → tasks → implement workflow works from the terminal.
- **[Greenfield Spring Boot MVC with a custom preset](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)** — Builds a Spring Boot MVC application from scratch using a custom pirate-speak preset, demonstrating how presets can reshape the entire spec-kit experience: specifications become "Voyage Manifests," plans become "Battle Plans," and tasks become "Crew Assignments" — all generated in full pirate vernacular without changing any tooling.
- **[Greenfield Spring Boot + React with a custom extension](https://github.com/mnriem/spec-kit-aide-extension-demo)** — Walks through the **AIDE extension**, a community extension that adds an alternative spec-driven workflow to spec-kit with high-level specs (vision) and low-level specs (work items) organized in a 7-step iterative lifecycle: vision → roadmap → progress tracking → work queue → work items → execution → feedback loops. Uses a family trading platform (Spring Boot 4, React 19, PostgreSQL, Docker Compose) as the scenario to illustrate how the extension mechanism lets you plug in a different style of spec-driven development without changing any core tooling — truly utilizing the "Kit" in Spec Kit.
See Spec-Driven Development in action across different scenarios with community-contributed walkthroughs; find the full list on the [Community Walkthroughs](https://github.github.io/spec-kit/community/walkthroughs.html) page.
## 🛠️ Community Friends
@@ -320,7 +311,7 @@ Run `specify integration list` to see all available integrations in your install
## Available Slash Commands
After running `specify init`, your AI coding agent will have access to these slash commands for structured development. If you pass `--ai <agent> --ai-skills`, Spec Kit installs agent skills instead of slash-command prompt files; `--ai-skills` requires `--ai`.
After running `specify init`, your AI coding agent will have access to these slash commands for structured development. For integrations that support skills mode, passing `--integration <agent> --integration-options="--skills"` installs agent skills instead of slash-command prompt files.
#### Core Commands
@@ -455,7 +446,7 @@ Our research and experimentation focus on:
- **Linux/macOS/Windows**
- [Supported](#-supported-ai-coding-agent-integrations) AI coding agent.
- [uv](https://docs.astral.sh/uv/) for package management
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pypa.github.io/pipx/) for persistent installation
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)
@@ -493,37 +484,37 @@ specify init --here --force
![Specify CLI bootstrapping a new project in the terminal](./media/specify_cli.gif)
You will be prompted to select the AI agent you are using. You can also proactively specify it directly in the terminal:
You will be prompted to select the coding agent integration you are using. You can also proactively specify it directly in the terminal:
```bash
specify init <project_name> --ai copilot
specify init <project_name> --ai gemini
specify init <project_name> --ai copilot
specify init <project_name> --integration copilot
specify init <project_name> --integration gemini
specify init <project_name> --integration codex
# Or in current directory:
specify init . --ai copilot
specify init . --ai codex --ai-skills
specify init . --integration copilot
specify init . --integration codex --integration-options="--skills"
# or use --here flag
specify init --here --ai copilot
specify init --here --ai codex --ai-skills
specify init --here --integration copilot
specify init --here --integration codex --integration-options="--skills"
# Force merge into a non-empty current directory
specify init . --force --ai copilot
specify init . --force --integration copilot
# or
specify init --here --force --ai copilot
specify init --here --force --integration copilot
```
The CLI will check if you have Claude Code, Gemini CLI, Cursor CLI, Qwen CLI, opencode, Codex CLI, Qoder CLI, Tabnine CLI, Kiro CLI, Pi, Forge, Goose, or Mistral Vibe installed. If you do not, or you prefer to get the templates without checking for the right tools, use `--ignore-agent-tools` with your command:
```bash
specify init <project_name> --ai copilot --ignore-agent-tools
specify init <project_name> --integration copilot --ignore-agent-tools
```
### **STEP 1:** Establish project principles
Go to the project folder and run your AI agent. In our example, we're using `claude`.
Go to the project folder and run your coding agent. In our example, we're using `claude`.
![Bootstrapping Claude Code environment](./media/bootstrap-claude-code.gif)
@@ -535,7 +526,7 @@ The first step should be establishing your project's governing principles using
/speckit.constitution Create principles focused on code quality, testing standards, user experience consistency, and performance requirements. Include governance for how these principles should guide technical decisions and implementation choices.
```
This step creates or updates the `.specify/memory/constitution.md` file with your project's foundational guidelines that the AI agent will reference during specification, planning, and implementation phases.
This step creates or updates the `.specify/memory/constitution.md` file with your project's foundational guidelines that the coding agent will reference during specification, planning, and implementation phases.
### **STEP 2:** Create project specifications
@@ -743,9 +734,9 @@ The `/speckit.implement` command will:
- Provide progress updates and handle errors appropriately
> [!IMPORTANT]
> The AI agent will execute local CLI commands (such as `dotnet`, `npm`, etc.) - make sure you have the required tools installed on your machine.
> The coding agent will execute local CLI commands (such as `dotnet`, `npm`, etc.) - make sure you have the required tools installed on your machine.
Once the implementation is complete, test the application and resolve any runtime errors that may not be visible in CLI logs (e.g., browser console errors). You can copy and paste such errors back to your AI agent for resolution.
Once the implementation is complete, test the application and resolve any runtime errors that may not be visible in CLI logs (e.g., browser console errors). You can copy and paste such errors back to your coding agent for resolution.
</details>

29
docs/community/presets.md Normal file
View File

@@ -0,0 +1,29 @@
# Community Presets
> [!NOTE]
> Community presets are independently created and maintained by their respective authors. Maintainers only verify that catalog entries are complete and correctly formatted — they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
The following community-contributed presets customize how Spec Kit behaves — overriding templates, commands, and terminology without changing any tooling. Presets are available in [`catalog.community.json`](https://github.com/github/spec-kit/blob/main/presets/catalog.community.json):
| Preset | Purpose | Provides | Requires | URL |
|--------|---------|----------|----------|-----|
| A11Y Governance | Adds WCAG 2.2 AA accessibility checks, bilingual DE/EN delivery, CEFR-B2 readability, CLI accessibility, and inclusive-content guidance | 9 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) |
| Agent Parity Governance | Keeps shared AI-agent instructions aligned across project-defined agent guidance surfaces and documents intentional deviations | 6 templates, 3 commands | — | [spec-kit-preset-agent-parity-governance](https://github.com/hindermath/spec-kit-preset-agent-parity-governance) |
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Architecture Governance | Adds secure architecture governance: trust boundaries, threat modeling, STRIDE/CAPEC, S-ADRs, Zero Trust applicability, and OWASP SAMM | 11 templates, 3 commands | — | [spec-kit-preset-architecture-governance](https://github.com/hindermath/spec-kit-preset-architecture-governance) |
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
| Claude AskUserQuestion | Upgrades `/speckit.clarify` and `/speckit.checklist` on Claude Code from Markdown-table prompts to the native AskUserQuestion picker, with a recommended option and reasoning on every question | 2 commands | — | [spec-kit-preset-claude-ask-questions](https://github.com/0xrafasec/spec-kit-preset-claude-ask-questions) |
| Cross-Platform Governance | Adds Bash/PowerShell parity, dry-run/WhatIf parity, Unix man-page expectations, PowerShell comment-based help, and Verb-Noun Cmdlet discipline | 8 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose. Supports interactive elements like brainstorming, interview, roleplay and extras like statistics, cover builder and bio command. Export with templates for KDP, D2D etc. | 22 templates, 27 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| iSAQB Architecture Governance | Adds general iSAQB/CPSA-F and arc42 architecture governance: goals, context, building blocks, runtime and deployment views, quality scenarios, ADRs, risks, and technical debt | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
| Jira Issue Tracking | Overrides `speckit.taskstoissues` to create Jira epics, stories, and tasks instead of GitHub Issues via Atlassian MCP tools | 1 command | — | [spec-kit-preset-jira](https://github.com/luno/spec-kit-preset-jira) |
| Multi-Repo Branching | Coordinates feature branch creation across multiple git repositories (independent repos and submodules) during plan and tasks phases | 2 commands | — | [spec-kit-preset-multi-repo-branching](https://github.com/sakitA/spec-kit-preset-multi-repo-branching) |
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Screenwriting | Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Spec Kit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks. Export to Fountain, FTX, PDF | 26 templates, 32 commands, 1 script | — | [speckit-preset-screenwriting](https://github.com/adaumann/speckit-preset-screenwriting) |
| Security Governance | Adds secure development governance: memory-safe-language preference, secure code generation, NIST SSDF, CWE Top 25, OWASP ASVS, SBOM/VEX/SLSA, OpenSSF Scorecard, and EU CRA applicability | 12 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy | 5 templates, 8 commands | — | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
| VS Code Ask Questions | Enhances the clarify command to use `vscode/askQuestions` for batched interactive questioning. | 1 command | — | [spec-kit-presets](https://github.com/fdcastel/spec-kit-presets) |
To build and publish your own preset, see the [Presets Publishing Guide](https://github.com/github/spec-kit/blob/main/presets/PUBLISHING.md).

View File

@@ -0,0 +1,20 @@
# Community Walkthroughs
> [!NOTE]
> Community walkthroughs are independently created and maintained by their respective authors. They are **not reviewed, nor endorsed, nor supported by GitHub**. Review their content before following along and use at your own discretion.
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs:
- **[Greenfield .NET CLI tool](https://github.com/mnriem/spec-kit-dotnet-cli-demo)** — Builds a Timezone Utility as a .NET single-binary CLI tool from a blank directory, covering the full spec-kit workflow: constitution, specify, plan, tasks, and multi-pass implement using GitHub Copilot agents.
- **[Greenfield Spring Boot + React platform](https://github.com/mnriem/spec-kit-spring-react-demo)** — Builds an LLM performance analytics platform (REST API, graphs, iteration tracking) from scratch using Spring Boot, embedded React, PostgreSQL, and Docker Compose, with a clarify step and a cross-artifact consistency analysis pass included.
- **[Brownfield ASP.NET CMS extension](https://github.com/mnriem/spec-kit-aspnet-brownfield-demo)** — Extends an existing open-source .NET CMS (CarrotCakeCMS-Core, ~307,000 lines of C#, Razor, SQL, JavaScript, and config files) with two new features — cross-platform Docker Compose infrastructure and a token-authenticated headless REST API — demonstrating how spec-kit fits into existing codebases without prior specs or a constitution.
- **[Brownfield Java runtime extension](https://github.com/mnriem/spec-kit-java-brownfield-demo)** — Extends an existing open-source Jakarta EE runtime (Piranha, ~420,000 lines of Java, XML, JSP, HTML, and config files across 180 Maven modules) with a password-protected Server Admin Console, demonstrating spec-kit on a large multi-module Java project with no prior specs or constitution.
- **[Brownfield Go / React dashboard demo](https://github.com/mnriem/spec-kit-go-brownfield-demo)** — Demonstrates spec-kit driven entirely from the **terminal using GitHub Copilot CLI**. Extends NASA's open-source Hermes ground support system (Go) with a lightweight React-based web telemetry dashboard, showing that the full constitution → specify → plan → tasks → implement workflow works from the terminal.
- **[Greenfield Spring Boot MVC with a custom preset](https://github.com/mnriem/spec-kit-pirate-speak-preset-demo)** — Builds a Spring Boot MVC application from scratch using a custom pirate-speak preset, demonstrating how presets can reshape the entire spec-kit experience: specifications become "Voyage Manifests," plans become "Battle Plans," and tasks become "Crew Assignments" — all generated in full pirate vernacular without changing any tooling.
- **[Greenfield Spring Boot + React with a custom extension](https://github.com/mnriem/spec-kit-aide-extension-demo)** — Walks through the **AIDE extension**, a community extension that adds an alternative spec-driven workflow to spec-kit with high-level specs (vision) and low-level specs (work items) organized in a 7-step iterative lifecycle: vision → roadmap → progress tracking → work queue → work items → execution → feedback loops. Uses a family trading platform (Spring Boot 4, React 19, PostgreSQL, Docker Compose) as the scenario to illustrate how the extension mechanism lets you plug in a different style of spec-driven development without changing any core tooling — truly utilizing the "Kit" in Spec Kit.

View File

@@ -4,7 +4,7 @@
- **Linux/macOS** (or Windows; PowerShell scripts now supported without WSL)
- AI coding agent: [Claude Code](https://www.anthropic.com/claude-code), [GitHub Copilot](https://code.visualstudio.com/), [Codebuddy CLI](https://www.codebuddy.ai/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Pi Coding Agent](https://pi.dev)
- [uv](https://docs.astral.sh/uv/) for package management
- [uv](https://docs.astral.sh/uv/) for package management (recommended) or [pipx](https://pypa.github.io/pipx/) for persistent installation
- [Python 3.11+](https://www.python.org/downloads/)
- [Git](https://git-scm.com/downloads)
@@ -24,6 +24,13 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <PROJE
uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME>
```
> [!NOTE]
> For a persistent installation, `pipx` works equally well:
> ```bash
> pipx install git+https://github.com/github/spec-kit.git@vX.Y.Z
> ```
> The project uses a standard `hatchling` build backend and has no uv-specific dependencies.
Or initialize in the current directory:
```bash
@@ -32,16 +39,16 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init .
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here
```
### Specify AI Agent
### Specify Integration
You can proactively specify your AI agent during initialization:
You can proactively specify your coding agent integration during initialization:
```bash
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai claude
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai gemini
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai codebuddy
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai pi
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --integration claude
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --integration gemini
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --integration copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --integration codebuddy
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --integration pi
```
### Specify Script Type (Shell vs PowerShell)
@@ -66,7 +73,7 @@ uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <proje
If you prefer to get the templates without checking for the right tools:
```bash
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --ai claude --ignore-agent-tools
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init <project_name> --integration claude --ignore-agent-tools
```
## Verification
@@ -79,7 +86,7 @@ specify version
This helps verify you are running the official Spec Kit build from GitHub, not an unrelated package with the same name.
After initialization, you should see the following commands available in your AI agent:
After initialization, you should see the following commands available in your coding agent:
- `/speckit.specify` - Create specifications
- `/speckit.plan` - Generate implementation plans
@@ -124,12 +131,10 @@ pip install --no-index --find-links=./dist specify-cli
```bash
# Initialize a project — no GitHub access needed
specify init my-project --ai claude --offline
specify init my-project --integration claude
```
The `--offline` flag tells the CLI to use the templates, commands, and scripts bundled inside the wheel instead of downloading from GitHub.
> **Deprecation notice:** Starting with v0.6.0, `specify init` will use bundled assets by default and the `--offline` flag will be removed. The GitHub download path will be retired because bundled assets eliminate the need for network access, avoid proxy/firewall issues, and guarantee that templates always match the installed CLI version. No action will be needed — `specify init` will simply work without network access out of the box.
Bundled assets are used by default — no network access is required.
> **Note:** Python 3.11+ is required.

View File

@@ -20,7 +20,7 @@ You can execute the CLI via the module entrypoint without installing anything:
```bash
# From repo root
python -m src.specify_cli --help
python -m src.specify_cli init demo-project --ai claude --ignore-agent-tools --script sh
python -m src.specify_cli init demo-project --integration claude --ignore-agent-tools --script sh
```
If you prefer invoking the script file style (uses shebang):
@@ -52,7 +52,7 @@ Re-running after code edits requires no reinstall because of editable mode.
`uvx` can run from a local path (or a Git ref) to simulate user flows:
```bash
uvx --from . specify init demo-uvx --ai copilot --ignore-agent-tools --script sh
uvx --from . specify init demo-uvx --integration copilot --ignore-agent-tools --script sh
```
You can also point uvx at a specific branch without merging:
@@ -69,14 +69,14 @@ If you're in another directory, use an absolute path instead of `.`:
```bash
uvx --from /mnt/c/GitHub/spec-kit specify --help
uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --ai copilot --ignore-agent-tools --script sh
uvx --from /mnt/c/GitHub/spec-kit specify init demo-anywhere --integration copilot --ignore-agent-tools --script sh
```
Set an environment variable for convenience:
```bash
export SPEC_KIT_SRC=/mnt/c/GitHub/spec-kit
uvx --from "$SPEC_KIT_SRC" specify init demo-env --ai copilot --ignore-agent-tools --script ps
uvx --from "$SPEC_KIT_SRC" specify init demo-env --integration copilot --ignore-agent-tools --script ps
```
(Optional) Define a shell function:
@@ -123,7 +123,7 @@ When testing `init --here` in a dirty directory, create a temp workspace:
```bash
mkdir /tmp/spec-test && cd /tmp/spec-test
python -m src.specify_cli init --here --ai claude --ignore-agent-tools --script sh # if repo copied here
python -m src.specify_cli init --here --integration claude --ignore-agent-tools --script sh # if repo copied here
```
Or copy only the modified CLI portion if you want a lighter sandbox.

View File

@@ -22,6 +22,17 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
uvx --from git+https://github.com/github/spec-kit.git specify init .
```
> [!NOTE]
> You can also install the CLI persistently with `pipx`:
> ```bash
> pipx install git+https://github.com/github/spec-kit.git
> ```
> After installing with `pipx`, run `specify` directly instead of `uvx --from ... specify`, for example:
> ```bash
> specify init <PROJECT_NAME>
> specify init .
> ```
Pick script type explicitly (optional):
```bash
@@ -31,7 +42,7 @@ uvx --from git+https://github.com/github/spec-kit.git specify init <PROJECT_NAME
### Step 2: Define Your Constitution
**In your AI Agent's chat interface**, use the `/speckit.constitution` slash command to establish the core rules and principles for your project. You should provide your project's specific principles as arguments.
**In your coding agent's chat interface**, use the `/speckit.constitution` slash command to establish the core rules and principles for your project. You should provide your project's specific principles as arguments.
```markdown
/speckit.constitution This project follows a "Library-First" approach. All features must be implemented as standalone libraries first. We use TDD strictly. We prefer functional programming patterns.
@@ -148,7 +159,7 @@ Generate an actionable task list using the `/speckit.tasks` command:
### Step 7: Validate and Implement
Have your AI agent audit the implementation plan using `/speckit.analyze`:
Have your coding agent audit the implementation plan using `/speckit.analyze`:
```bash
/speckit.analyze
@@ -169,7 +180,7 @@ Finally, implement the solution:
- **Don't focus on tech stack** during specification phase
- **Iterate and refine** your specifications before implementation
- **Validate** the plan before coding begins
- **Let the AI agent handle** the implementation details
- **Let the coding agent handle** the implementation details
## Next Steps

View File

@@ -22,6 +22,10 @@ specify init [<project_name>]
Creates a new Spec Kit project with the necessary directory structure, templates, scripts, and AI coding agent integration files.
> [!NOTE]
> The git extension is currently enabled by default during `specify init`.
> Starting in `v0.10.0`, it will require explicit opt-in. To add it after init, run `specify extension add git`.
Use `<project_name>` to create a new directory, or `--here` (or `.`) to initialize in the current directory. If the directory already has files, use `--force` to merge without confirmation.
### Examples

View File

@@ -13,6 +13,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify
| [CodeBuddy CLI](https://www.codebuddy.ai/cli) | `codebuddy` | |
| [Codex CLI](https://github.com/openai/codex) | `codex` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `$speckit-<command>` |
| [Cursor](https://cursor.sh/) | `cursor-agent` | |
| [Devin for Terminal](https://cli.devin.ai/docs) | `devin` | Skills-based integration; installs skills into `.devin/skills/` and invokes them as `/speckit-<command>` |
| [Forge](https://forgecode.dev/) | `forge` | |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `gemini` | |
| [GitHub Copilot](https://code.visualstudio.com/) | `copilot` | |
@@ -42,6 +43,8 @@ specify integration list
```
Shows all available integrations, which one is currently installed, and whether each requires a CLI tool or is IDE-based.
When multiple integrations are installed, the list marks the default integration separately from the other installed integrations.
The list also shows whether each built-in integration is declared multi-install safe.
## Install an Integration
@@ -52,9 +55,12 @@ specify integration install <key>
| Option | Description |
| ------------------------ | ------------------------------------------------------------------------ |
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--force` | Opt in to installing alongside integrations that are not declared multi-install safe |
| `--integration-options` | Integration-specific options (e.g. `--integration-options="--commands-dir .myagent/cmds"`) |
Installs the specified integration into the current project. Fails if another integration is already installed — use `switch` instead. If the installation fails partway through, it automatically rolls back to a clean state.
Installs the specified integration into the current project. If another integration is already installed, the command only proceeds automatically when all involved integrations are declared multi-install safe. Otherwise, use `switch` to replace the default integration or pass `--force` to explicitly opt in to multi-install. If the installation fails partway through, it automatically rolls back to a clean state.
Installing an additional integration does not change the default integration. Use `specify integration use <key>` to change the default.
> **Note:** All integration management commands require a project already initialized with `specify init`. To start a new project with a specific agent, use `specify init <project> --integration <key>` instead.
@@ -83,10 +89,22 @@ specify integration switch <key>
| Option | Description |
| ------------------------ | ------------------------------------------------------------------------ |
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--force` | Force removal of modified files during uninstall |
| `--integration-options` | Options for the target integration |
| `--force` | Force removal of modified files during uninstall; when the target is already installed, overwrite managed shared templates while changing the default |
| `--integration-options` | Options for the target integration when it is not already installed |
Equivalent to running `uninstall` followed by `install` in a single step.
If the target integration is not already installed, equivalent to running `uninstall` followed by `install` in a single step. In this mode, `--force` controls whether modified files from the removed integration are deleted. If the target integration is already installed, `switch` only changes the default integration, like `use`; in this mode, `--force` controls whether managed shared templates are overwritten while the default changes. `--integration-options` is rejected for already-installed targets because changing integration options requires reinstalling managed files; run `upgrade <key> --integration-options ...` first, then `use <key>`.
## Use an Installed Integration
```bash
specify integration use <key>
```
| Option | Description |
| --------- | --------------------------------------------------- |
| `--force` | Overwrite managed shared templates while changing the default |
Sets the default integration without uninstalling any other installed integrations. This also refreshes managed shared templates so command references match the new default integration's invocation style. Modified or untracked shared templates are preserved unless `--force` is used.
## Upgrade an Integration
@@ -100,7 +118,7 @@ specify integration upgrade [<key>]
| `--script sh\|ps` | Script type: `sh` (bash/zsh) or `ps` (PowerShell) |
| `--integration-options` | Options for the integration |
Reinstalls the current integration with updated templates and commands (e.g., after upgrading Spec Kit). Defaults to the currently installed integration; if a key is provided, it must match the installed one — otherwise the command fails and suggests using `switch` instead. Detects locally modified files and blocks the upgrade unless `--force` is used. Stale files from the previous install that are no longer needed are removed automatically.
Reinstalls an installed integration with updated templates and commands (e.g., after upgrading Spec Kit). Defaults to the default integration; if a key is provided, it must be one of the installed integrations. Detects locally modified files and blocks the upgrade unless `--force` is used. Stale files from the previous install that are no longer needed are removed automatically. Shared templates stay aligned with the default integration even when upgrading a non-default integration.
## Integration-Specific Options
@@ -119,9 +137,39 @@ specify integration install generic --integration-options="--commands-dir .myage
## FAQ
### Can I use multiple integrations at the same time?
### Can I install multiple integrations in the same project?
No. Only one AI coding agent integration can be installed per project. Use `specify integration switch <key>` to change to a different AI coding agent.
Yes, but it is intended for team portability rather than the default workflow. Multiple integrations are allowed automatically only when the installed integration and the new integration are declared multi-install safe by Spec Kit. For other combinations, pass `--force` to acknowledge that multiple agents may see unrelated agent-specific instructions or commands.
Spec Kit tracks one default integration in `.specify/integration.json` with `default_integration`, all installed integrations with `installed_integrations`, per-integration runtime settings with `integration_settings`, and a dedicated `integration_state_schema` for future state migrations. The legacy `integration` field remains as an alias for the default integration.
### Which integrations are multi-install safe?
An integration is multi-install safe when it uses isolated agent directories, a dedicated context file that does not collide with another safe integration, stable command invocation settings, and a separate install manifest. Shared Spec Kit templates remain aligned to the single default integration.
The currently declared multi-install safe integrations are:
| Key | Isolation |
| --- | --------- |
| `auggie` | `.augment/commands`, `.augment/rules/specify-rules.md` |
| `claude` | `.claude/skills`, `CLAUDE.md` |
| `codebuddy` | `.codebuddy/commands`, `CODEBUDDY.md` |
| `codex` | `.agents/skills`, `AGENTS.md` |
| `cursor-agent` | `.cursor/skills`, `.cursor/rules/specify-rules.mdc` |
| `gemini` | `.gemini/commands`, `GEMINI.md` |
| `iflow` | `.iflow/commands`, `IFLOW.md` |
| `junie` | `.junie/commands`, `.junie/AGENTS.md` |
| `kilocode` | `.kilocode/workflows`, `.kilocode/rules/specify-rules.md` |
| `kimi` | `.kimi/skills`, `KIMI.md` |
| `qodercli` | `.qoder/commands`, `QODER.md` |
| `qwen` | `.qwen/commands`, `QWEN.md` |
| `roo` | `.roo/commands`, `.roo/rules/specify-rules.md` |
| `shai` | `.shai/commands`, `SHAI.md` |
| `tabnine` | `.tabnine/agent/commands`, `TABNINE.md` |
| `trae` | `.trae/skills`, `.trae/rules/project_rules.md` |
| `windsurf` | `.windsurf/workflows`, `.windsurf/rules/specify-rules.md` |
Integrations that share a context file or command directory with another integration, require dynamic install paths such as `--commands-dir`, or merge shared tool settings are not declared safe by default. They can still be installed alongside another integration with `--force`.
### What happens to my changes when I uninstall or switch?
@@ -137,4 +185,4 @@ CLI-based integrations (like Claude Code, Gemini CLI) require the tool to be ins
### When should I use `upgrade` vs `switch`?
Use `upgrade` when you've upgraded Spec Kit and want to refresh the same integration's templates. Use `switch` when you want to change to a different AI coding agent.
Use `upgrade` when you've upgraded Spec Kit and want to refresh an installed integration's managed files. Use `switch` when you want to replace the current default with another integration; if the target is already installed, `switch` behaves like `use`.

View File

@@ -37,5 +37,9 @@
# Community
- name: Community
items:
- name: Presets
href: community/presets.md
- name: Walkthroughs
href: community/walkthroughs.md
- name: Friends
href: community/friends.md

View File

@@ -9,7 +9,8 @@
| What to Upgrade | Command | When to Use |
|----------------|---------|-------------|
| **CLI Tool Only** | `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git@vX.Y.Z` | Get latest CLI features without touching project files |
| **Project Files** | `specify init --here --force --ai <your-agent>` | Update slash commands, templates, and scripts in your project |
| **CLI Tool Only (pipx)** | `pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z` | Reinstall/upgrade a pipx-installed CLI to a specific release |
| **Project Files** | `specify init --here --force --integration <your-agent>` | Update slash commands, templates, and scripts in your project |
| **Both** | Run CLI upgrade, then project update | Recommended for major version updates |
---
@@ -31,7 +32,15 @@ uv tool install specify-cli --force --from git+https://github.com/github/spec-ki
Specify the desired release tag:
```bash
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --ai copilot
uvx --from git+https://github.com/github/spec-kit.git@vX.Y.Z specify init --here --integration copilot
```
### If you installed with `pipx`
Upgrade to a specific release:
```bash
pipx install --force git+https://github.com/github/spec-kit.git@vX.Y.Z
```
### Verify the upgrade
@@ -53,8 +62,8 @@ When Spec Kit releases new features (like new slash commands or updated template
Running `specify init --here --force` will update:
-**Slash command files** (`.claude/commands/`, `.github/prompts/`, etc.)
-**Script files** (`.specify/scripts/`)
-**Template files** (`.specify/templates/`)
-**Script files** (`.specify/scripts/`)**only with `--force`**; without it, only missing files are added
-**Template files** (`.specify/templates/`)**only with `--force`**; without it, only missing files are added
-**Shared memory files** (`.specify/memory/`) - **⚠️ See warnings below**
### What stays safe?
@@ -73,7 +82,7 @@ The `specs/` directory is completely excluded from template packages and will ne
Run this inside your project directory:
```bash
specify init --here --force --ai <your-agent>
specify init --here --force --integration <your-agent>
```
Replace `<your-agent>` with your AI coding agent. Refer to this list of [Supported AI Coding Agent Integrations](reference/integrations.md)
@@ -81,7 +90,7 @@ Replace `<your-agent>` with your AI coding agent. Refer to this list of [Support
**Example:**
```bash
specify init --here --force --ai copilot
specify init --here --force --integration copilot
```
### Understanding the `--force` flag
@@ -94,7 +103,9 @@ Template files will be merged with existing content and may overwrite existing f
Proceed? [y/N]
```
With `--force`, it skips the confirmation and proceeds immediately.
With `--force`, it skips the confirmation and proceeds immediately. It also **overwrites shared infrastructure files** (`.specify/scripts/` and `.specify/templates/`) with the latest versions from the installed Spec Kit release.
Without `--force`, shared infrastructure files that already exist are skipped — the CLI will print a warning listing the skipped files so you know which ones were not updated.
**Important: Your `specs/` directory is always safe.** The `--force` flag only affects template files (commands, scripts, templates, memory). Your feature specifications, plans, and tasks in `specs/` are never included in upgrade packages and cannot be overwritten.
@@ -113,7 +124,7 @@ With `--force`, it skips the confirmation and proceeds immediately.
cp .specify/memory/constitution.md .specify/memory/constitution-backup.md
# 2. Run the upgrade
specify init --here --force --ai copilot
specify init --here --force --integration copilot
# 3. Restore your customized constitution
mv .specify/memory/constitution-backup.md .specify/memory/constitution.md
@@ -126,13 +137,14 @@ Or use git to restore it:
git restore .specify/memory/constitution.md
```
### 2. Custom template modifications
### 2. Custom script or template modifications
If you customized any templates in `.specify/templates/`, the upgrade will overwrite them. Back them up first:
If you customized files in `.specify/scripts/` or `.specify/templates/`, the `--force` flag will overwrite them. Back them up first:
```bash
# Back up custom templates
# Back up custom templates and scripts
cp -r .specify/templates .specify/templates-backup
cp -r .specify/scripts .specify/scripts-backup
# After upgrade, merge your changes back manually
```
@@ -170,7 +182,7 @@ Restart your IDE to refresh the command list.
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# Update project files to get new commands
specify init --here --force --ai copilot
specify init --here --force --integration copilot
# Restore your constitution if customized
git restore .specify/memory/constitution.md
@@ -187,7 +199,7 @@ cp -r .specify/templates /tmp/templates-backup
uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git
# 3. Update project
specify init --here --force --ai copilot
specify init --here --force --integration copilot
# 4. Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
@@ -220,7 +232,7 @@ If you initialized your project with `--no-git`, you can still upgrade:
cp .specify/memory/constitution.md /tmp/constitution-backup.md
# Run upgrade
specify init --here --force --ai copilot --no-git
specify init --here --force --integration copilot --no-git
# Restore customizations
mv /tmp/constitution-backup.md .specify/memory/constitution.md
@@ -241,13 +253,13 @@ The `--no-git` flag tells Spec Kit to **skip git repository initialization**. Th
**During initial setup:**
```bash
specify init my-project --ai copilot --no-git
specify init my-project --integration copilot --no-git
```
**During upgrade:**
```bash
specify init --here --force --ai copilot --no-git
specify init --here --force --integration copilot --no-git
```
### What `--no-git` does NOT do
@@ -355,7 +367,7 @@ Only Spec Kit infrastructure files:
- **Use `--force` flag** - Skip this confirmation entirely:
```bash
specify init --here --force --ai copilot
specify init --here --force --integration copilot
```
**When you see this warning:**

View File

@@ -528,11 +528,9 @@ specify extension add <extension-name> --from https://github.com/.../spec-kit-my
Submit to the community catalog for public discovery:
1. **Fork** spec-kit repository
2. **Add entry** to `extensions/catalog.community.json`
3. **Update** the Community Extensions table in `README.md` with your extension
4. **Create PR** following the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md)
5. **After merge**, your extension becomes available:
1. **Create a GitHub release** for your extension
2. **File an issue** using the [Extension Submission](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml) template
3. **After review**, a maintainer updates the catalog and your extension becomes available:
- Users can browse `catalog.community.json` to discover your extension
- Users copy the entry to their own `catalog.json`
- Users install with: `specify extension add my-ext` (from their catalog)
@@ -669,7 +667,7 @@ hooks:
**Error**: `Extension requires spec-kit >=0.2.0`
- **Fix**: Update spec-kit with `uv tool install specify-cli --force`
- **Fix**: Update spec-kit with `uv tool install specify-cli --force --from git+https://github.com/github/spec-kit.git`. The bare `specify-cli` package on PyPI is a different, unrelated project — installing it without `--from git+...` will give you a stub CLI that does not include `extension`, `preset`, or other spec-kit commands.
**Error**: `Command file not found`

View File

@@ -7,9 +7,8 @@ This guide explains how to publish your extension to the Spec Kit extension cata
1. [Prerequisites](#prerequisites)
2. [Prepare Your Extension](#prepare-your-extension)
3. [Submit to Catalog](#submit-to-catalog)
4. [Verification Process](#verification-process)
5. [Release Workflow](#release-workflow)
6. [Best Practices](#best-practices)
4. [Release Workflow](#release-workflow)
5. [Best Practices](#best-practices)
---
@@ -133,222 +132,46 @@ specify extension add <extension-name> --from https://github.com/your-org/spec-k
Spec Kit uses a dual-catalog system. For details about how catalogs work, see the main [Extensions README](README.md#extension-catalogs).
**For extension publishing**: All community extensions should be added to `catalog.community.json`. Users browse this catalog and copy extensions they trust into their own `catalog.json`.
**For extension publishing**: All community extensions are listed in `extensions/catalog.community.json`. Users browse this catalog and copy extensions they trust into their own `catalog.json`.
### 1. Fork the spec-kit Repository
### How to Submit
```bash
# Fork on GitHub
# https://github.com/github/spec-kit/fork
To submit your extension to the community catalog, file a new issue using the **[Extension Submission](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml)** template. The template collects all required metadata, including:
# Clone your fork
git clone https://github.com/YOUR-USERNAME/spec-kit.git
cd spec-kit
```
- Extension ID, name, and version
- Description, author, and license
- Repository, download URL, and documentation links
- Required Spec Kit version and any tool dependencies
- Number of commands and hooks
- Tags and key features
- Testing confirmation
### 2. Add Extension to Community Catalog
> [!IMPORTANT]
> Do **not** open a pull request directly to edit `extensions/catalog.community.json`. All community extension submissions must go through the issue template so a maintainer can review the entry and update the catalog.
Edit `extensions/catalog.community.json` and add your extension:
### What Happens After You Submit
```json
{
"schema_version": "1.0",
"updated_at": "2026-01-28T15:54:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"your-extension": {
"name": "Your Extension Name",
"id": "your-extension",
"description": "Brief description of your extension",
"author": "Your Name",
"version": "1.0.0",
"download_url": "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/your-org/spec-kit-your-extension",
"homepage": "https://github.com/your-org/spec-kit-your-extension",
"documentation": "https://github.com/your-org/spec-kit-your-extension/blob/main/docs/",
"changelog": "https://github.com/your-org/spec-kit-your-extension/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "required-mcp-tool",
"version": ">=1.0.0",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 1
},
"tags": [
"category",
"tool-name",
"feature"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-01-28T00:00:00Z",
"updated_at": "2026-01-28T00:00:00Z"
}
}
}
```
1. Your issue is automatically labeled and assigned to a maintainer for review
2. A maintainer verifies that the catalog entry is complete and correctly formatted
3. Once approved, the maintainer adds your extension to `extensions/catalog.community.json` and the Community Extensions table in the README
4. Your extension becomes discoverable via `specify extension search`
**Important**:
### What Maintainers Check
- Set `verified: false` (maintainers will verify)
- Set `downloads: 0` and `stars: 0` (auto-updated later)
- Use current timestamp for `created_at` and `updated_at`
- Update the top-level `updated_at` to current time
- The catalog entry fields are complete and correctly formatted
- The download URL is accessible
- The repository exists and contains an `extension.yml` manifest
### 3. Update Community Extensions Table
Add your extension to the Community Extensions table in the project root `README.md`:
```markdown
| Your Extension Name | Brief description of what it does | `<category>` | <effect> | [repo-name](https://github.com/your-org/spec-kit-your-extension) |
```
**(Table) Category** — pick the one that best fits your extension:
- `docs` — reads, validates, or generates spec artifacts
- `code` — reviews, validates, or modifies source code
- `process` — orchestrates workflow across phases
- `integration` — syncs with external platforms
- `visibility` — reports on project health or progress
**Effect** — choose one:
- Read-only — produces reports without modifying files
- Read+Write — modifies files, creates artifacts, or updates specs
Insert your extension in alphabetical order in the table.
### 4. Submit Pull Request
```bash
# Create a branch
git checkout -b add-your-extension
# Commit your changes
git add extensions/catalog.community.json README.md
git commit -m "Add your-extension to community catalog
- Extension ID: your-extension
- Version: 1.0.0
- Author: Your Name
- Description: Brief description
"
# Push to your fork
git push origin add-your-extension
# Create Pull Request on GitHub
# https://github.com/github/spec-kit/compare
```
**Pull Request Template**:
```markdown
## Extension Submission
**Extension Name**: Your Extension Name
**Extension ID**: your-extension
**Version**: 1.0.0
**Author**: Your Name
**Repository**: https://github.com/your-org/spec-kit-your-extension
### Description
Brief description of what your extension does.
### Checklist
- [x] Valid extension.yml manifest
- [x] README.md with installation and usage docs
- [x] LICENSE file included
- [x] GitHub release created (v1.0.0)
- [x] Extension tested on real project
- [x] All commands working
- [x] No security vulnerabilities
- [x] Added to extensions/catalog.community.json
- [x] Added to Community Extensions table in README.md
### Testing
Tested on:
- macOS 13.0+ with spec-kit 0.1.0
- Project: [Your test project]
### Additional Notes
Any additional context or notes for reviewers.
```
---
## Verification Process
### What Happens After Submission
1. **Automated Checks** (if available):
- Manifest validation
- Download URL accessibility
- Repository existence
- License file presence
2. **Manual Review**:
- Code quality review
- Security audit
- Functionality testing
- Documentation review
3. **Verification**:
- If approved, `verified: true` is set
- Extension appears in `specify extension search --verified`
### Verification Criteria
To be verified, your extension must:
**Functionality**:
- Works as described in documentation
- All commands execute without errors
- No breaking changes to user workflows
**Security**:
- No known vulnerabilities
- No malicious code
- Safe handling of user data
- Proper validation of inputs
**Code Quality**:
- Clean, readable code
- Follows extension best practices
- Proper error handling
- Helpful error messages
**Documentation**:
- Clear installation instructions
- Usage examples
- Troubleshooting section
- Accurate description
**Maintenance**:
- Active repository
- Responsive to issues
- Regular updates
- Semantic versioning followed
> [!NOTE]
> Maintainers do **not** review, audit, or test the extension code itself.
### Typical Review Timeline
- **Automated checks**: Immediate (if implemented)
- **Manual review**: 3-7 business days
- **Verification**: After successful review
- **Review**: 3-7 business days
### Updating an Existing Extension
To update an extension that is already in the catalog (e.g., for a new version), file a new **[Extension Submission](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml)** issue with the updated version, download URL, and any other changed fields. Mention in the issue that this is an update to an existing entry.
---
@@ -385,26 +208,7 @@ When releasing a new version:
# Create release on GitHub
```
4. **Update catalog**:
```bash
# Fork spec-kit repo (or update existing fork)
cd spec-kit
# Update extensions/catalog.json
jq '.extensions["your-extension"].version = "1.1.0"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].download_url = "https://github.com/your-org/spec-kit-your-extension/archive/refs/tags/v1.1.0.zip"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.extensions["your-extension"].updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
jq '.updated_at = "2026-02-15T00:00:00Z"' extensions/catalog.json > tmp.json && mv tmp.json extensions/catalog.json
# Submit PR
git checkout -b update-your-extension-v1.1.0
git add extensions/catalog.json
git commit -m "Update your-extension to v1.1.0"
git push origin update-your-extension-v1.1.0
```
5. **Submit update PR** with changelog in description
4. **File an update submission** using the [Extension Submission](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml) template with the new version and download URL. Mention in the issue that this is an update to an existing entry.
---
@@ -473,9 +277,9 @@ A: The main catalog is for public extensions only. For private extensions:
- Users add your catalog: `specify extension add-catalog https://your-domain.com/catalog.json`
- Not yet implemented - coming in Phase 4
### Q: How long does verification take?
### Q: How long does review take?
A: Typically 3-7 business days for initial review. Updates to verified extensions are usually faster.
A: Typically 3-7 business days. Updates to existing extensions are usually faster.
### Q: What if my extension is rejected?
@@ -483,11 +287,11 @@ A: You'll receive feedback on what needs to be fixed. Make the changes and resub
### Q: Can I update my extension anytime?
A: Yes, submit a PR to update the catalog with your new version. Verified status may be re-evaluated for major changes.
A: Yes, file a new [Extension Submission](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml) issue with the updated version and download URL. Mention that it is an update to an existing entry.
### Q: Do I need to be verified to be in the catalog?
A: No, unverified extensions are still searchable. Verification just adds trust and visibility.
A: No. All community extensions are listed in the catalog once their submission is reviewed and accepted.
### Q: Can extensions have paid features?
@@ -536,7 +340,7 @@ A: Extensions should be free and open-source. Commercial support/services are al
"hooks": "integer (optional)"
},
"tags": ["array of strings (2-10 tags)"],
"verified": "boolean (default: false)",
"verified": "boolean (default: false, set by maintainers)",
"downloads": "integer (auto-updated)",
"stars": "integer (auto-updated)",
"created_at": "string (ISO 8601 datetime)",

View File

@@ -153,7 +153,7 @@ This will:
2. Validate the manifest
3. Check compatibility with your spec-kit version
4. Install to `.specify/extensions/jira/`
5. Register commands with your AI agent
5. Register commands with your coding agent
6. Create config template
### Install from URL
@@ -189,7 +189,7 @@ Provided commands:
### Automatic Agent Skill Registration
If your project was initialized with `--ai-skills`, extension commands are **automatically registered as agent skills** during installation. This ensures that extensions are discoverable by agents that use the [agentskills.io](https://agentskills.io) skill specification.
If your project uses a skills-based integration (e.g., `--integration claude`, `--integration codex`) or was initialized with `--integration-options="--skills"`, extension commands are **automatically registered as agent skills** during installation. This ensures that extensions are discoverable by agents that use the [agentskills.io](https://agentskills.io) skill specification.
```text
✓ Extension installed successfully!
@@ -208,7 +208,7 @@ When an extension is removed, its corresponding skills are also cleaned up autom
### Using Extension Commands
Extensions add commands that appear in your AI agent (Claude Code):
Extensions add commands that appear in your coding agent (Claude Code):
```text
# In Claude Code
@@ -423,7 +423,7 @@ In addition to extension-specific environment variables (`SPECKIT_{EXT_ID}_*`),
| Variable | Description | Default |
|----------|-------------|---------|
| `SPECKIT_CATALOG_URL` | Override the full catalog stack with a single URL (backward compat) | Built-in default stack |
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub API token for downloads | None |
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token for authenticated requests to GitHub-hosted URLs (`raw.githubusercontent.com`, `github.com`, `api.github.com`, `codeload.github.com`). Required when your catalog JSON or extension ZIPs are hosted in a private GitHub repository. | None |
#### Example: Using a custom catalog for testing
@@ -435,6 +435,21 @@ export SPECKIT_CATALOG_URL="http://localhost:8000/catalog.json"
export SPECKIT_CATALOG_URL="https://example.com/staging/catalog.json"
```
#### Example: Using a private GitHub-hosted catalog
```bash
# Authenticate with a token (gh CLI, PAT, or GITHUB_TOKEN in CI)
export GITHUB_TOKEN=$(gh auth token)
# Search a private catalog added via `specify extension catalog add`
specify extension search jira
# Install from a private catalog
specify extension add jira-sync
```
The token is attached automatically to requests targeting GitHub domains. Non-GitHub catalog URLs are always fetched without credentials.
---
## Extension Catalogs
@@ -780,12 +795,12 @@ specify extension add --dev /path/to/extension
### Command Not Available
**Issue**: Extension command not appearing in AI agent
**Issue**: Extension command not appearing in coding agent
**Solutions**:
1. Check extension is enabled: `specify extension list`
2. Restart AI agent (Claude Code)
2. Restart coding agent (Claude Code)
3. Check command file exists:
```bash
@@ -819,8 +834,8 @@ specify extension add --dev /path/to/extension
**Solutions**:
1. Check MCP server is installed
2. Check AI agent MCP configuration
3. Restart AI agent
2. Check coding agent MCP configuration
3. Restart coding agent
4. Check extension requirements: `specify extension info jira`
### Permission Denied

View File

@@ -25,13 +25,13 @@ specify extension search # Now uses your organization's catalog instead of the
### Community Reference Catalog (`catalog.community.json`)
> [!NOTE]
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. Review extension source code before installation and use at your own discretion.
> Community extensions are independently created and maintained by their respective authors. Maintainers only verify that catalog entries are complete and correctly formatted — they do **not review, audit, endorse, or support the extension code itself**. Review extension source code before installation and use at your own discretion.
- **Purpose**: Browse available community-contributed extensions
- **Status**: Active - contains extensions submitted by the community
- **Location**: `extensions/catalog.community.json`
- **Usage**: Reference catalog for discovering available extensions
- **Submission**: Open to community contributions via Pull Request
- **Submission**: Open to community contributions via [issue template](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml)
**How It Works:**
@@ -72,7 +72,7 @@ specify extension add <extension-name> --from https://github.com/org/spec-kit-ex
## Available Community Extensions
> [!NOTE]
> Community extensions are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
> Community extensions are independently created and maintained by their respective authors. Maintainers only verify that catalog entries are complete and correctly formatted — they do **not review, audit, endorse, or support the extension code itself**. The Community Extensions website is also a third-party resource. Review extension source code before installation and use at your own discretion.
🔍 **Browse and search community extensions on the [Community Extensions website](https://speckit-community.github.io/extensions/).**
@@ -89,10 +89,8 @@ To add your extension to the community catalog:
1. **Prepare your extension** following the [Extension Development Guide](EXTENSION-DEVELOPMENT-GUIDE.md)
2. **Create a GitHub release** for your extension
3. **Submit a Pull Request** that:
- Adds your extension to `extensions/catalog.community.json`
- Updates this README with your extension in the Available Extensions table
4. **Wait for review** - maintainers will review and merge if criteria are met
3. **File an issue** using the [Extension Submission](https://github.com/github/spec-kit/issues/new?template=extension_submission.yml) template with all required metadata
4. **Wait for review** — a maintainer will review the submission, update the catalog, and close the issue
See the [Extension Publishing Guide](EXTENSION-PUBLISHING-GUIDE.md) for detailed step-by-step instructions.

View File

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-04-21T00:00:00Z",
"updated_at": "2026-05-05T07:26:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"aide": {
@@ -100,6 +100,39 @@
"created_at": "2026-04-14T00:00:00Z",
"updated_at": "2026-04-14T00:00:00Z"
},
"architecture-guard": {
"name": "Architecture Guard",
"id": "architecture-guard",
"description": "Continuous architecture governance for AI-assisted development. Reviews specs, plans, and code for architecture drift, producing structured refactor tasks and evolution proposals.",
"author": "DyanGalih",
"version": "1.4.0",
"download_url": "https://github.com/DyanGalih/spec-kit-architecture-guard/archive/refs/tags/v1.4.0.zip",
"repository": "https://github.com/DyanGalih/spec-kit-architecture-guard",
"homepage": "https://github.com/DyanGalih/spec-kit-architecture-guard",
"documentation": "https://github.com/DyanGalih/spec-kit-architecture-guard/blob/main/README.md",
"changelog": "https://github.com/DyanGalih/spec-kit-architecture-guard/releases",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 6,
"hooks": 0
},
"tags": [
"architecture",
"governance",
"drift-detection",
"refactor",
"monolithic",
"microservices"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-05-05T07:26:00Z",
"updated_at": "2026-05-05T07:26:00Z"
},
"archive": {
"name": "Archive Extension",
"id": "archive",
@@ -657,18 +690,18 @@
"id": "extensify",
"description": "Create and validate extensions and extension catalogs.",
"author": "mnriem",
"version": "1.0.0",
"download_url": "https://github.com/mnriem/spec-kit-extensions/releases/download/extensify-v1.0.0/extensify.zip",
"version": "1.1.0",
"download_url": "https://github.com/mnriem/spec-kit-extensions/releases/download/extensify-v1.1.0/extensify.zip",
"repository": "https://github.com/mnriem/spec-kit-extensions",
"homepage": "https://github.com/mnriem/spec-kit-extensions",
"documentation": "https://github.com/mnriem/spec-kit-extensions/blob/main/extensify/README.md",
"changelog": "https://github.com/mnriem/spec-kit-extensions/blob/main/extensify/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0"
"speckit_version": ">=0.8.0"
},
"provides": {
"commands": 4,
"commands": 5,
"hooks": 0
},
"tags": [
@@ -681,7 +714,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-03-18T00:00:00Z",
"updated_at": "2026-03-18T00:00:00Z"
"updated_at": "2026-04-23T00:00:00Z"
},
"fix-findings": {
"name": "Fix Findings",
@@ -941,6 +974,44 @@
"created_at": "2026-03-17T00:00:00Z",
"updated_at": "2026-03-17T00:00:00Z"
},
"m365": {
"name": "Microsoft 365 Integration",
"id": "m365",
"description": "Fetch Teams messages, meeting transcripts, and SharePoint/OneDrive files as local Markdown for spec generation.",
"author": "BenBtg",
"version": "1.0.0",
"download_url": "https://github.com/BenBtg/spec-kit-m365/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/BenBtg/spec-kit-m365",
"homepage": "https://github.com/BenBtg/spec-kit-m365",
"documentation": "https://github.com/BenBtg/spec-kit-m365/blob/main/README.md",
"changelog": "https://github.com/BenBtg/spec-kit-m365/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "m365",
"required": true
}
]
},
"provides": {
"commands": 3,
"hooks": 0
},
"tags": [
"microsoft-365",
"teams",
"transcripts",
"collaboration",
"summarization"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-28T00:00:00Z",
"updated_at": "2026-04-28T00:00:00Z"
},
"maqa": {
"name": "MAQA — Multi-Agent & Quality Assurance",
"id": "maqa",
@@ -1167,6 +1238,45 @@
"created_at": "2026-03-26T00:00:00Z",
"updated_at": "2026-03-26T00:00:00Z"
},
"markitdown": {
"name": "MarkItDown Document Converter",
"id": "markitdown",
"description": "Convert documents (PDF, Word, PowerPoint, Excel, and more) to Markdown for use as spec reference material in Spec Kit workflows.",
"author": "BenBtg",
"version": "1.0.0",
"download_url": "https://github.com/BenBtg/spec-kit-markitdown/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/BenBtg/spec-kit-markitdown",
"homepage": "https://github.com/BenBtg/spec-kit-markitdown",
"documentation": "https://github.com/BenBtg/spec-kit-markitdown/blob/main/README.md",
"changelog": "https://github.com/BenBtg/spec-kit-markitdown/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "markitdown",
"version": ">=0.1.0",
"required": true
}
]
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"markdown",
"pdf",
"document-conversion",
"reference-material",
"extraction"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-28T00:00:00Z",
"updated_at": "2026-04-28T00:00:00Z"
},
"memory-loader": {
"name": "Memory Loader",
"id": "memory-loader",
@@ -1198,6 +1308,39 @@
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z"
},
"memory-md": {
"name": "Memory MD",
"id": "memory-md",
"description": "Spec Kit extension for repository-native Markdown memory that captures durable decisions, bugs, and project context",
"author": "DyanGalih",
"version": "0.7.5",
"download_url": "https://github.com/DyanGalih/spec-kit-memory-hub/archive/refs/tags/v0.7.5.zip",
"repository": "https://github.com/DyanGalih/spec-kit-memory-hub",
"homepage": "https://github.com/DyanGalih/spec-kit-memory-hub",
"documentation": "https://github.com/DyanGalih/spec-kit-memory-hub/blob/main/README.md",
"changelog": "https://github.com/DyanGalih/spec-kit-memory-hub/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 6,
"hooks": 0
},
"tags": [
"memory",
"workflow",
"docs",
"copilot",
"markdown",
"ai-context"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-23T00:00:00Z",
"updated_at": "2026-05-03T00:00:00Z"
},
"memorylint": {
"name": "MemoryLint",
"id": "memorylint",
@@ -1230,6 +1373,56 @@
"created_at": "2026-04-09T00:00:00Z",
"updated_at": "2026-04-16T13:10:26Z"
},
"multi-model-review": {
"name": "Multi-Model Review",
"id": "multi-model-review",
"description": "Cross-model Spec Kit handoffs for spec authoring, implementation routing, and review.",
"author": "formin",
"version": "0.1.0",
"download_url": "https://github.com/formin/multi-model-review/archive/refs/tags/v0.1.0.zip",
"repository": "https://github.com/formin/multi-model-review",
"homepage": "https://github.com/formin/multi-model-review",
"documentation": "https://github.com/formin/multi-model-review/blob/main/README.md",
"changelog": "https://github.com/formin/multi-model-review/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0",
"tools": [
{
"name": "git",
"required": true
},
{
"name": "codex",
"required": false
},
{
"name": "gemini",
"required": false
},
{
"name": "claude",
"required": false
}
]
},
"provides": {
"commands": 4,
"hooks": 0
},
"tags": [
"review",
"workflow",
"multi-model",
"spec-driven-development",
"code"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-05-04T02:51:52Z",
"updated_at": "2026-05-04T02:51:52Z"
},
"onboard": {
"name": "Onboard",
"id": "onboard",
@@ -1295,6 +1488,38 @@
"created_at": "2026-04-03T00:00:00Z",
"updated_at": "2026-04-03T00:00:00Z"
},
"orchestrator": {
"name": "Spec Orchestrator",
"id": "orchestrator",
"description": "Cross-feature orchestration — track state, select tasks, and detect conflicts across parallel specs.",
"author": "Quratulain-bilal",
"version": "1.0.0",
"download_url": "https://github.com/Quratulain-bilal/spec-kit-orchestrator/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/Quratulain-bilal/spec-kit-orchestrator",
"homepage": "https://github.com/Quratulain-bilal/spec-kit-orchestrator",
"documentation": "https://github.com/Quratulain-bilal/spec-kit-orchestrator/blob/main/README.md",
"changelog": "https://github.com/Quratulain-bilal/spec-kit-orchestrator/releases",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 4,
"hooks": 0
},
"tags": [
"orchestration",
"multi-feature",
"coordination",
"workflow",
"parallel"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-24T14:00:00Z",
"updated_at": "2026-04-24T14:00:00Z"
},
"plan-review-gate": {
"name": "Plan Review Gate",
"id": "plan-review-gate",
@@ -1392,10 +1617,10 @@
"product-forge": {
"name": "Product Forge",
"id": "product-forge",
"description": "Full product lifecycle: research → product spec → SpecKit → implement → verify → test",
"description": "Full product lifecycle from research to release — portfolio, lite mode, monorepo, optional V-Model",
"author": "VaiYav",
"version": "1.1.1",
"download_url": "https://github.com/VaiYav/speckit-product-forge/archive/refs/tags/v1.1.1.zip",
"version": "1.5.1",
"download_url": "https://github.com/VaiYav/speckit-product-forge/archive/refs/tags/v1.5.1.zip",
"repository": "https://github.com/VaiYav/speckit-product-forge",
"homepage": "https://github.com/VaiYav/speckit-product-forge",
"documentation": "https://github.com/VaiYav/speckit-product-forge/blob/main/README.md",
@@ -1405,21 +1630,21 @@
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 10,
"commands": 29,
"hooks": 0
},
"tags": [
"process",
"research",
"product-spec",
"lifecycle",
"testing"
"monorepo",
"v-model",
"portfolio"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-03-28T00:00:00Z",
"updated_at": "2026-03-28T00:00:00Z"
"updated_at": "2026-04-24T15:52:00Z"
},
"qa": {
"name": "QA Testing Extension",
@@ -1456,12 +1681,12 @@
"id": "ralph",
"description": "Autonomous implementation loop using AI agent CLI.",
"author": "Rubiss",
"version": "1.0.1",
"download_url": "https://github.com/Rubiss/spec-kit-ralph/archive/refs/tags/v1.0.1.zip",
"repository": "https://github.com/Rubiss/spec-kit-ralph",
"homepage": "https://github.com/Rubiss/spec-kit-ralph",
"documentation": "https://github.com/Rubiss/spec-kit-ralph/blob/main/README.md",
"changelog": "https://github.com/Rubiss/spec-kit-ralph/blob/main/CHANGELOG.md",
"version": "1.0.2",
"download_url": "https://github.com/Rubiss-Projects/spec-kit-ralph/archive/refs/tags/v1.0.2.zip",
"repository": "https://github.com/Rubiss-Projects/spec-kit-ralph",
"homepage": "https://github.com/Rubiss-Projects/spec-kit-ralph",
"documentation": "https://github.com/Rubiss-Projects/spec-kit-ralph/blob/main/README.md",
"changelog": "https://github.com/Rubiss-Projects/spec-kit-ralph/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
@@ -1490,7 +1715,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-03-09T00:00:00Z",
"updated_at": "2026-04-12T19:00:00Z"
"updated_at": "2026-05-04T17:02:08Z"
},
"reconcile": {
"name": "Reconcile Extension",
@@ -1523,6 +1748,38 @@
"created_at": "2026-03-14T00:00:00Z",
"updated_at": "2026-03-14T00:00:00Z"
},
"red-team": {
"name": "Red Team",
"id": "red-team",
"description": "Adversarial review of functional specs before /speckit.plan. Parallel adversarial lens agents catch hostile actors, silent failures, and regulatory blind spots that clarify/analyze cannot.",
"author": "Ash Brener",
"version": "1.0.2",
"download_url": "https://github.com/ashbrener/spec-kit-red-team/releases/download/v1.0.2/red-team-v1.0.2.zip",
"repository": "https://github.com/ashbrener/spec-kit-red-team",
"homepage": "https://github.com/ashbrener/spec-kit-red-team",
"documentation": "https://github.com/ashbrener/spec-kit-red-team/blob/main/README.md",
"changelog": "https://github.com/ashbrener/spec-kit-red-team/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 2,
"hooks": 1
},
"tags": [
"adversarial-review",
"quality-gate",
"spec-hardening",
"pre-plan",
"audit"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
},
"refine": {
"name": "Spec Refine",
"id": "refine",
@@ -1756,10 +2013,10 @@
"security-review": {
"name": "Security Review",
"id": "security-review",
"description": "Comprehensive security audit of codebases using AI-powered DevSecOps analysis",
"description": "Full-project secure-by-design security audits plus staged, branch/PR, plan, task, follow-up, and apply reviews",
"author": "DyanGalih",
"version": "1.1.1",
"download_url": "https://github.com/DyanGalih/spec-kit-security-review/archive/refs/tags/v1.1.1.zip",
"version": "1.4.2",
"download_url": "https://github.com/DyanGalih/spec-kit-security-review/archive/refs/tags/v1.4.2.zip",
"repository": "https://github.com/DyanGalih/spec-kit-security-review",
"homepage": "https://github.com/DyanGalih/spec-kit-security-review",
"documentation": "https://github.com/DyanGalih/spec-kit-security-review/blob/main/README.md",
@@ -1769,7 +2026,7 @@
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 3,
"commands": 7,
"hooks": 0
},
"tags": [
@@ -1783,7 +2040,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-04-03T03:24:03Z",
"updated_at": "2026-04-03T04:15:00Z"
"updated_at": "2026-05-03T00:00:00Z"
},
"sf": {
"name": "SFSpeckit — Salesforce Spec-Driven Development",
@@ -1922,6 +2179,38 @@
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-21T00:00:00Z"
},
"spec2cloud": {
"name": "Spec2Cloud",
"id": "spec2cloud",
"description": "Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy.",
"author": "Azure Samples",
"version": "1.1.0",
"download_url": "https://github.com/Azure-Samples/Spec2Cloud/releases/download/spec-kit-spec2cloud-v1.1.0/extension.zip",
"repository": "https://github.com/Azure-Samples/Spec2Cloud",
"homepage": "https://aka.ms/spec2cloud",
"documentation": "https://github.com/Azure-Samples/Spec2Cloud/blob/main/spec-kit/README.md",
"changelog": "https://github.com/Azure-Samples/Spec2Cloud/blob/main/spec-kit/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.4.0"
},
"provides": {
"commands": 2,
"hooks": 0
},
"tags": [
"spec2cloud",
"azure",
"cloud",
"deploy",
"workflow"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-30T00:00:00Z",
"updated_at": "2026-04-30T00:00:00Z"
},
"speckit-utils": {
"name": "SDD Utilities",
"id": "speckit-utils",
@@ -1987,6 +2276,45 @@
"created_at": "2026-04-10T16:00:00Z",
"updated_at": "2026-04-10T16:00:00Z"
},
"squad": {
"name": "Squad Bridge",
"id": "squad",
"description": "Bootstrap and synchronize a Squad agent team from your Spec Kit spec and tasks.",
"author": "jwill824",
"version": "1.1.0",
"download_url": "https://github.com/jwill824/spec-kit-squad/archive/refs/tags/v1.1.0.zip",
"repository": "https://github.com/jwill824/spec-kit-squad",
"homepage": "https://github.com/jwill824/spec-kit-squad",
"documentation": "https://github.com/jwill824/spec-kit-squad/blob/main/README.md",
"changelog": "https://github.com/jwill824/spec-kit-squad/blob/main/docs/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "@bradygaster/squad-cli",
"version": ">=0.1.0",
"required": true
}
]
},
"provides": {
"commands": 4,
"hooks": 2
},
"tags": [
"multi-agent",
"agents",
"orchestration",
"process",
"integration"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-29T00:00:00Z",
"updated_at": "2026-04-29T00:00:00Z"
},
"staff-review": {
"name": "Staff Review Extension",
"id": "staff-review",
@@ -2122,6 +2450,39 @@
"created_at": "2026-03-30T00:00:00Z",
"updated_at": "2026-04-16T14:08:23Z"
},
"superpowers-bridge": {
"name": "Superpowers Bridge",
"id": "superpowers-bridge",
"description": "Bridges spec-kit workflows with obra/superpowers capabilities for brainstorming, TDD, code review, and resumable execution.",
"author": "WangX0111",
"version": "1.0.0",
"download_url": "https://github.com/WangX0111/superspec/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/WangX0111/superspec",
"homepage": "https://github.com/WangX0111/superspec",
"documentation": "https://github.com/WangX0111/superspec/blob/main/README.md",
"changelog": "https://github.com/WangX0111/superspec/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"commands": 5,
"hooks": 3
},
"tags": [
"superpowers",
"brainstorming",
"tdd",
"code-review",
"subagent",
"workflow"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
},
"sync": {
"name": "Spec Sync",
"id": "sync",
@@ -2186,13 +2547,76 @@
"created_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-10T00:00:00Z"
},
"threatmodel": {
"name": "OWASP LLM Threat Model",
"id": "threatmodel",
"description": "OWASP Top 10 for LLM Applications 2025 threat analysis on agent artifacts",
"author": "NaviaSamal",
"version": "1.0.0",
"download_url": "https://github.com/NaviaSamal/spec-kit-threatmodel/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/NaviaSamal/spec-kit-threatmodel",
"homepage": "https://github.com/NaviaSamal/spec-kit-threatmodel",
"documentation": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/README.md",
"changelog": "https://github.com/NaviaSamal/spec-kit-threatmodel/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 1,
"hooks": 1
},
"tags": [
"security",
"owasp",
"threat-model",
"llm",
"analysis"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-25T00:00:00Z",
"updated_at": "2026-04-25T00:00:00Z"
},
"token-analyzer": {
"name": "Token Consumption Analyzer",
"id": "token-analyzer",
"description": "Captures, analyzes, and compares token consumption across SDD workflows",
"author": "Chris Roberts | coderandhiker",
"version": "0.1.0",
"download_url": "https://github.com/coderandhiker/spec-kit-token-analyzer/archive/refs/tags/v0.1.0.zip",
"repository": "https://github.com/coderandhiker/spec-kit-token-analyzer",
"homepage": "https://github.com/coderandhiker/spec-kit-token-analyzer",
"documentation": "https://github.com/coderandhiker/spec-kit-token-analyzer/blob/main/README.md",
"changelog": "https://github.com/coderandhiker/spec-kit-token-analyzer/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 3,
"hooks": 4
},
"tags": [
"tokens",
"measurement",
"optimization",
"analysis"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-05-01T00:00:00Z",
"updated_at": "2026-05-01T00:00:00Z"
},
"v-model": {
"name": "V-Model Extension Pack",
"id": "v-model",
"description": "Enforces V-Model paired generation of development specs and test specs with full traceability.",
"author": "leocamello",
"version": "0.5.0",
"download_url": "https://github.com/leocamello/spec-kit-v-model/archive/refs/tags/v0.5.0.zip",
"version": "0.6.0",
"download_url": "https://github.com/leocamello/spec-kit-v-model/archive/refs/tags/v0.6.0.zip",
"repository": "https://github.com/leocamello/spec-kit-v-model",
"homepage": "https://github.com/leocamello/spec-kit-v-model",
"documentation": "https://github.com/leocamello/spec-kit-v-model/blob/main/README.md",
@@ -2214,9 +2638,9 @@
],
"verified": false,
"downloads": 0,
"stars": 0,
"stars": 21,
"created_at": "2026-02-20T00:00:00Z",
"updated_at": "2026-04-06T00:00:00Z"
"updated_at": "2026-04-25T00:00:00Z"
},
"verify": {
"name": "Verify Extension",
@@ -2286,8 +2710,8 @@
"id": "version-guard",
"description": "Verify tech stack versions against live registries before planning and implementation",
"author": "KevinBrown5280",
"version": "1.0.0",
"download_url": "https://github.com/KevinBrown5280/spec-kit-version-guard/archive/refs/tags/v1.0.0.zip",
"version": "1.2.0",
"download_url": "https://github.com/KevinBrown5280/spec-kit-version-guard/archive/refs/tags/v1.2.0.zip",
"repository": "https://github.com/KevinBrown5280/spec-kit-version-guard",
"homepage": "https://github.com/KevinBrown5280/spec-kit-version-guard",
"documentation": "https://github.com/KevinBrown5280/spec-kit-version-guard/blob/main/README.md",
@@ -2297,8 +2721,8 @@
"speckit_version": ">=0.2.0"
},
"provides": {
"commands": 1,
"hooks": 2
"commands": 3,
"hooks": 4
},
"tags": [
"versioning",
@@ -2310,7 +2734,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z"
"updated_at": "2026-04-22T21:10:00Z"
},
"whatif": {
"name": "What-if Analysis",
@@ -2340,6 +2764,85 @@
"created_at": "2026-04-13T00:00:00Z",
"updated_at": "2026-04-13T00:00:00Z"
},
"wireframe": {
"name": "Wireframe Visual Feedback Loop",
"id": "wireframe",
"description": "SVG wireframe generation, review, and sign-off for spec-driven development. Approved wireframes become spec constraints honored by /speckit.plan, /speckit.tasks, and /speckit.implement.",
"author": "TortoiseWolfe",
"version": "0.1.1",
"download_url": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/archive/refs/tags/v0.1.1.zip",
"repository": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe",
"homepage": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe",
"documentation": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/blob/main/README.md",
"changelog": "https://github.com/TortoiseWolfe/spec-kit-extension-wireframe/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 6,
"hooks": 3
},
"tags": [
"wireframe",
"visual",
"design",
"ui",
"mockup",
"svg",
"feedback-loop",
"sign-off"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-22T00:00:00Z",
"updated_at": "2026-04-22T00:00:00Z"
},
"workiq": {
"name": "Work IQ",
"id": "workiq",
"description": "Integrate Microsoft 365 organizational knowledge into spec-driven development workflows",
"author": "sakitA",
"version": "1.0.0",
"download_url": "https://github.com/sakitA/spec-kit-workiq/archive/refs/tags/v1.0.0.zip",
"repository": "https://github.com/sakitA/spec-kit-workiq",
"homepage": "https://github.com/sakitA/spec-kit-workiq",
"documentation": "https://github.com/sakitA/spec-kit-workiq/blob/main/README.md",
"changelog": "https://github.com/sakitA/spec-kit-workiq/blob/main/CHANGELOG.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0",
"tools": [
{
"name": "workiq",
"version": ">=1.0.0",
"required": true
},
{
"name": "node",
"version": ">=18.0.0",
"required": true
}
]
},
"provides": {
"commands": 4,
"hooks": 2
},
"tags": [
"microsoft-365",
"work-iq",
"context",
"integration",
"productivity"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-04-29T00:00:00Z",
"updated_at": "2026-04-29T00:00:00Z"
},
"worktree": {
"name": "Worktree Isolation",
"id": "worktree",
@@ -2402,7 +2905,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-04-13T00:00:00Z",
"updated_at": "2026-04-13T00:00:00Z"
"updated_at": "2026-04-13T00:00:00Z"
}
}
}

View File

@@ -95,7 +95,7 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
fi
# Trim whitespace and validate description is not empty
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
exit 1

View File

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-04-08T00:00:00Z",
"updated_at": "2026-04-28T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/integrations/catalog.json",
"integrations": {
"claude": {
@@ -66,6 +66,15 @@
"repository": "https://github.com/github/spec-kit",
"tags": ["cli", "skills"]
},
"devin": {
"id": "devin",
"name": "Devin for Terminal",
"version": "1.0.0",
"description": "Devin for Terminal CLI skills-based integration",
"author": "spec-kit-core",
"repository": "https://github.com/github/spec-kit",
"tags": ["cli", "skills"]
},
"qwen": {
"id": "qwen",
"name": "Qwen Code",

147
newsletters/2026-April.md Normal file
View File

@@ -0,0 +1,147 @@
# Spec Kit - April 2026 Newsletter
This edition covers Spec Kit activity in April 2026. Seventeen releases shipped (v0.4.4 through v0.8.3), delivering a full integration plugin architecture, a workflow engine, preset composition strategies, an integration catalog, and comprehensive documentation. The community extension catalog tripled from 26 to 83 entries, community presets grew from 2 to 12, and Spec Kit appeared on the Thoughtworks Technology Radar. A summary is in the table below, followed by details.
| **Spec Kit Core (Apr 2026)** | **Community & Content** | **SDD Ecosystem & Next** |
| --- | --- | --- |
| Seventeen releases shipped with major features: integration plugin architecture, workflow engine, preset composition, integration catalog, bundled lean preset, documentation site, and academic citation support. Three new agents added (Forgecode, Goose, Devin for Terminal). The repo grew from ~82k to **92,038 stars**. [\[github.com\]](https://github.com/github/spec-kit/releases) | Thoughtworks Technology Radar placed Spec Kit in the "Assess" ring. Community catalog grew from 26 to **83 extensions** and from 2 to **12 presets**. 12 substantive external articles published. XB Software documented a real legacy project. Fabián Silva shipped the Caramelo VS Code extension. | Matt Rickard argued for "smaller specs, harder checks." Will Torber's three-framework comparison recommended OpenSpec for most teams. The "Spec Layer" debate emerged: specs as constraint surfaces for AI agents. Spec Kit leads in breadth and portability; competitors differentiate on drift detection and orchestration depth. |
***
> **Important:** April's release pace outran external coverage. Most analyses published during the month (Rickard on April 1, Thoughtworks Radar on April 15, XB Software on April 17, Torber on April 23) were evaluating versions that predated the workflow engine (v0.7.0), integration catalog (v0.7.2), preset composition (v0.8.0), and catalog discovery CLI (v0.8.3). The ceremony and flexibility concerns they raised are precisely what these features address — the lean preset, pluggable workflows, composable presets, and community extensions like Conduct, MAQA, and Fleet Orchestrator already deliver alternative workflows beyond the default SDD process. We look forward to seeing how upcoming reviews account for these capabilities.
## Spec Kit Project Updates
### Releases Overview
**v0.4.4** (April 1) delivered the first stage of the **integration plugin architecture** — base classes, a manifest system, and a registry that replaced the hard-coded agent scaffolding. It also added the Product Forge, Superpowers Bridge, MAQA suite (7 extensions), Spec Kit Onboard, and Plan Review Gate to the community catalog, fixed Claude Code CLI detection for npm-local installs, and added `--allow-existing-branch` to `create-new-feature`. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.4.4)
**v0.4.5** (April 2) completed the integration migration in five stages: standard markdown integrations for 19 agents, TOML integrations (Gemini, Tabnine), skills and generic integrations, and removal of the legacy scaffold path. It also installed Claude Code as native skills, added a `--dry-run` flag for `create-new-feature`, support for 4+ digit feature branch numbers, the Fix Findings extension, and five lifecycle extensions to the community catalog. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.4.5)
**v0.5.0** (April 2) was a significant packaging change: **template zip bundles were removed from releases**, with the CLI itself now handling all scaffolding. This ensured CLI and templates stay in sync. It also introduced `DEVELOPMENT.md` for contributor onboarding. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.5.0)
**v0.5.1** (April 8) was a large patch release. It added the **bundled Git extension** (stages 1 and 2) with hooks on all core commands and `GIT_BRANCH_NAME` override support, **Forgecode** agent support, and the `specify integration` subcommand for post-init integration management. Argument hints were added to Claude Code commands. Numerous community extensions joined the catalog (Confluence, Canon, Spec Diagram, Branch Convention, Spec Refine, FixIt, Optimize, Security Review) along with presets (explicit-task-dependencies, toc-navigation, VS Code Ask Questions). Bug fixes included pinning typer≥0.24.0/click≥8.2.1 to fix an import crash, BSD-portable sed escaping, Trae agent fix, TOML frontmatter stripping, and preventing ambiguous TOML closing quotes. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.5.1)
**v0.6.0** (April 9) rewrote **AGENTS.md for the new integration architecture**, added the SpecKit Companion to Community Friends, and brought Bugfix Workflow, Worktree Isolation, and MemoryLint to the community catalog. A new multi-repo-branching preset arrived. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.6.0)
**v0.6.1** (April 10) added the **bundled lean preset** with a minimal workflow command set — a lighter-weight alternative to the full SDD ceremony. It also migrated **Cursor** from `.cursor/commands` to `.cursor/skills` and added Brownfield Bootstrap, CI Guard, SpecTest, PR Bridge, TinySpec, and Status Report to the community catalog. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.6.1)
**v0.6.2** (April 13) added **Goose AI agent** support (YAML-based recipe format), the GitHub Issues Integration extension, and the What-if Analysis extension. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.6.2)
**v0.7.0** (April 14) delivered the **workflow engine with catalog system**, enabling pluggable, multi-step workflow definitions. It added SFSpeckit (Salesforce SDD), the Worktrees extension, optional single-segment branch prefix for gitflow compatibility, and the claude-ask-questions and fiction-book-writing presets. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.7.0)
**v0.7.1** (April 15) deprecated the `--ai` flag in favor of `--integration` on `specify init`, added Windows to the CI test matrix, fixed Claude skill chaining for hook execution, merged TESTING.md into CONTRIBUTING.md, and added the Agent Assign and Architect Preview extensions. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.7.1)
**v0.7.2** (April 16) delivered the **integration catalog** for discovery, versioning, and community distribution of agent integrations. It also produced a major **documentation overhaul**: reference pages for core commands, extensions, presets, workflows, and integrations were added to `docs/reference/`, and the README CLI section was simplified. The Issues extension and Catalog CI extension joined the community catalog. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.7.2)
**v0.7.3** (April 17) replaced shell-based context updates with a **marker-based upsert** mechanism, eliminating accidental context file bloat. It added a **Community Friends page** to the docs site, the Spec Scope and Blueprint extensions, and a Claude Code/Copilot CLI plugin marketplace reference in the README. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.7.3)
**v0.7.4** (April 21) added **CITATION.cff and .zenodo.json** for academic citation support. It introduced Ripple (side-effect detection), Spec Validate, Version Guard, Spec Reference Loader, and Memory Loader extensions. A fix stripped UTF-8 BOM from agent context files, and the Antigravity (agy) agent layout was migrated to `.agents/` with `--skills` deprecated. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.7.4)
**v0.7.5** (April 22) added `specify self check` and `self upgrade` stubs, the **preset wrap strategy** (completing the composition trifecta alongside prepend and append), the Red Team adversarial review extension, the Wireframe extension, and a **directory traversal security fix** in command write paths. Skill placeholder resolution was expanded to all SKILL.md agents. Community content (walkthroughs and presets) was moved from the README to the docs site. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.7.5)
**v0.8.0** (April 23) delivered **preset composition strategies** (prepend, append, wrap) for templates, commands, and scripts — enabling presets to layer content around existing artifacts. It also added Copilot `--integration-options="--skills"` for skills-based scaffolding, `pipx` as an alternative installation method, and the Memory MD extension. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.8.0)
**v0.8.1** (April 24) fixed `/speckit.plan` on custom git branches via `.specify/feature.json`, migrated the **Mistral Vibe** integration to SkillsIntegration, added the **Screenwriting** and **Jira** presets, and resolved command reference formats per integration type (dot vs. hyphen notation). [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.8.1)
**v0.8.2** (April 28) introduced **GITHUB_TOKEN/GH_TOKEN authentication** for private catalog and extension downloads, deprecated the `--no-git` flag (removal gated at v0.10.0), replaced all deprecated `--ai` references with `--integration` in documentation, and added MarkItDown Document Converter, Microsoft 365 Integration, Spec Orchestrator, and the Fiction Book Writing v1.7 preset with RAG (Chroma DB) offline semantic search. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.8.2)
**v0.8.3** (April 29) closed the month with **catalog discovery CLI commands** (search, info, catalog list/add/remove), support for **Devin for Terminal** as a skills-based integration, a fix for the opencode command dispatch, and the OWASP LLM Threat Model, iSAQB Architecture Governance, and Work IQ extensions. A fix was also added to the upgrade hint to prevent users from accidentally installing a PyPI squat package. [\[github.com\]](https://github.com/github/spec-kit/releases/tag/v0.8.3)
### Architecture & Infrastructure Highlights
The most significant architectural change in April was the **integration plugin architecture** (v0.4.4v0.4.5), which replaced hard-coded agent scaffolding with a registry of self-describing integration classes. Each agent is now a self-contained subpackage under `src/specify_cli/integrations/<key>/` with base classes for Markdown, TOML, YAML, and Skills formats. This six-stage migration touched all 28 supported agents and laid the groundwork for the integration catalog (v0.7.2) and community-distributed integrations.
The **workflow engine** (v0.7.0) introduced a catalog-based system for pluggable, multi-step workflow definitions — moving beyond the fixed seven-step SDD sequence.
**Preset composition strategies** (v0.7.5/v0.8.0) completed the preset system with prepend, append, and wrap modes. Presets can now layer content around existing templates, commands, and scripts rather than only replacing them.
The **marker-based context upsert** (v0.7.3) replaced fragile shell-based sed operations for updating agent context files, eliminating a class of bugs around context bloat and encoding issues.
**Template zip bundles were removed** (v0.5.0), coupling the CLI and templates into a single distributable artifact.
### Bug Fixes and Security
The most critical fix was **blocking directory traversal in command write paths** (#2229, v0.7.5), which prevented a potential path traversal vulnerability in the CommandRegistrar. Other security-adjacent fixes included hardening against a **PyPI squat package** in upgrade hints (v0.8.3) and adding **GITHUB_TOKEN authentication** for private catalog downloads (v0.8.2).
Notable bug fixes: typer/click import crash (v0.5.1), BSD-portable sed escaping (v0.5.1), UTF-8 BOM stripping from context files (v0.7.4), CRLF warning suppression in PowerShell auto-commit (v0.7.3), Claude skill chaining for hooks (v0.7.1), TOML ambiguous closing quotes (v0.5.1), and custom branch support for `/speckit.plan` (v0.8.1). [\[github.com\]](https://github.com/github/spec-kit/releases)
### The Extension & Preset Ecosystem
The community extension catalog **tripled** during April, growing from 26 to **83 entries**. 59 new extensions were added and 2 were removed (Cognitive Squad and Understanding, whose repositories were no longer available). Community presets grew from 2 to **12 entries**, with 10 new presets added.
Notable new extensions by category:
- **Project management**: GitHub Issues Integration (Fatima367, aaronrsun), Spec Orchestrator (Quratulain-bilal), Agent Assign (xuyang), Status Report (Open-Agent-Tools)
- **Quality & security**: Red Team adversarial review (Ash Brener), Security Review (DyanGalih), Ripple side-effect detection (chordpli), Spec Validate (Ahmed Eltayeb), CI Guard (Quratulain-bilal), OWASP LLM Threat Model (NaviaSamal)
- **Multi-agent & orchestration**: MAQA suite with 7 extensions covering multi-agent QA, Jira, Azure DevOps, GitHub Projects, Linear, and Trello integrations (GenieRobot), Product Forge (VaiYav)
- **Spec lifecycle**: Spec Refine (Quratulain-bilal), Bugfix Workflow (Quratulain-bilal), Fix Findings (Quratulain-bilal), Brownfield Bootstrap (Quratulain-bilal), TinySpec (Quratulain-bilal)
- **Developer experience**: Blueprint code review (chordpli), Confluence (aaronrsun), MarkItDown Document Converter (BenBtg), Microsoft 365 Integration (BenBtg), Memory MD (DyanGalih), Memory Loader (KevinBrown5280), MemoryLint (RbBtSn0w)
- **Domain-specific**: SFSpeckit for Salesforce (Sumanth Yanamala), iSAQB Architecture Governance preset (Thorsten Hindermann), Canon baseline-driven workflows (Maxim Stupakov)
- **Creative**: Fiction Book Writing preset v1.7 with RAG/Chroma DB support (Andreas Daumann), Screenwriting preset (Andreas Daumann)
Notable contributor **Quratulain-bilal** contributed 15 extensions during the month, spanning spec lifecycle, workflow management, and CI/CD integration. **GenieRobot** contributed the 7-extension MAQA suite. **BenBtg** contributed both MarkItDown and Microsoft 365 integrations. [\[github.com\]](https://github.com/github/spec-kit/releases)
### Documentation Overhaul
April saw a comprehensive documentation effort. Reference pages for **core commands, extensions, presets, workflows, and integrations** were created under `docs/reference/`. Community content — **walkthroughs, presets, and a Community Friends page** — was moved from the README to `docs/community/`, reducing README length while improving discoverability. The deprecated `--ai` flag references were replaced with `--integration` across all documentation. TESTING.md was merged into CONTRIBUTING.md, and `DEVELOPMENT.md` was introduced for contributor onboarding. [\[github.com\]](https://github.com/github/spec-kit/releases)
## Community & Content
### Thoughtworks Technology Radar
On **April 15**, the **Thoughtworks Technology Radar Volume 34** placed GitHub Spec Kit in the **"Assess" ring** under Languages & Frameworks. The blip noted that teams report value in brownfield projects, that the constitution captures project scope and architecture, but flagged potential **instruction bloat, context rot, and verbose markdown output** as concerns to watch. This is the first appearance of any SDD-specific tool on the Radar. [\[thoughtworks.com\]](https://www.thoughtworks.com/radar/languages-and-frameworks/github-spec-kit)
### Developer Articles and Blog Posts
April produced 12 substantive external articles (plus one excluded as AI-generated SEO spam).
**Matt Rickard** published *"The Spec Layer: Why Spec-Driven Development (SDD) Works"* on April 1. His thesis: specs reduce execution freedom for AI agents, functioning as constraint surfaces. He compared Spec Kit, Kiro, OpenSpec, Tessl, Intent, and Symphony, and advocated for **"smaller specs, harder checks, less guessing."** [\[blog.matt-rickard.com\]](https://blog.matt-rickard.com/p/the-spec-layer)
**Fabián Silva** published *"I Built a Visual Spec-Driven Development Extension for VS Code That Works With Any LLM"* on April 3 on DEV Community. His **Caramelo** VS Code extension adds a visual UI, approval gates, Jira integration, and multi-LLM support on top of Spec Kit's workflow, reading and writing the standard `specs/` directory. [\[dev.to\]](https://dev.to/fabian_silva_/i-built-a-visual-spec-driven-development-extension-for-vs-code-that-works-with-any-llm-36ok)
**James M** published *"GitHub Spec Kit in 2026: SDD Goes Mainstream"* on April 4, calling the transition "from framework to platform" and highlighting Claude Code native skills, multi-agent support, and the massive ecosystem growth. [\[jamesm.blog\]](https://jamesm.blog/ai/github-spec-kit-2026-update/)
**Peter Saktor** published a detailed tutorial on DEV Community on April 6: *"GitHub Spec-Kit: From Vibe Coding to Spec-Driven Development,"* walking through a full 7-step SDD workflow refactoring an Azure Container App with 33 tasks across 6 phases. [\[dev.to\]](https://dev.to/petersaktor/github-spec-kit-from-vibe-coding-to-spec-driven-development-1pgd)
**Codexplorer** published *"Spec Kit: GitHub's Answer to 'The AI Built the Wrong Thing Again'"* on Medium (April 11), framing Spec Kit as flipping the spec-code relationship, with Go code examples covering the seven slash commands. [\[medium.com\]](https://codexplorer.medium.com/spec-kit-githubs-answer-to-the-ai-built-the-wrong-thing-again-22f122f142fb)
**XB Software** published *"Spec Kit on a Real Project: Implementation Experience in Large Legacy Code"* on April 17 — a field report from applying SDD to legacy systems. A week-long task was completed in half the time. The AI surfaced hidden requirements gaps. They noted API integration weakness, that SDD is overkill for small tasks, and that an experienced reviewer is still essential. [\[xbsoftware.com\]](https://xbsoftware.com/blog/ai-in-legacy-systems-spec-driven-development/)
**What IT Is** published *"Perspectives in Spec Driven Development"* on April 21, surveying the SDD landscape (Spec Kit, Kiro, Tessl) and calling Spec Kit "a good entry point." [\[theitsolutionist.com\]](https://theitsolutionist.com/2026/04/21/perspectives-in-spec-driven-development/)
**Will Torber** published *"Spec Kit vs BMAD vs OpenSpec: Choosing an SDD Framework in 2026"* on DEV Community on April 23. He recommended Spec Kit for greenfield but flagged brownfield friction and the branch-per-spec limitation, ultimately **recommending OpenSpec for most teams**. [\[dev.to\]](https://dev.to/willtorber/spec-kit-vs-bmad-vs-openspec-choosing-an-sdd-framework-in-2026-d3j)
**Truong Phung** published *"Spec Kit vs. Superpowers: A Comprehensive Comparison & Practical Guide to Combining Both"* on DEV Community on April 25 — an 11-section comparison proposing a hybrid workflow: "Spec Kit plans WHAT, Superpowers controls HOW," with a step-by-step playbook. [\[dev.to\]](https://dev.to/truongpx396/spec-kit-vs-superpowers-a-comprehensive-comparison-practical-guide-to-combining-both-52jj)
**Markus Wondrak** published *"Re-evaluating GitHub's Spec Kit: Structured SDLC Automation"* on LinkedIn on April 26, examining Spec Kit as a structured SDLC automation approach requiring human review at phase boundaries. [\[linkedin.com\]](https://www.linkedin.com/pulse/re-evaluating-githubs-spec-kit-structured-sdlc-markus-wondrak-eewqf/)
**FintechExtra** published a factual release-notes summary of v0.8.2 on April 28, highlighting authenticated catalog downloads, the UTF-8 manifest fix, and the Chroma DB semantic search in the fiction writing preset. [\[fintechextra.com\]](https://www.fintechextra.com/news/github-spec-kit-v082-expands-catalog-support-and-tightens-cli-behavior-331)
### Community Friends and Tools
The **SpecKit Companion** VS Code extension was added to the Community Friends section (v0.6.0). A community-maintained plugin for **Claude Code and GitHub Copilot CLI** that installs Spec Kit skills via the plugin marketplace was referenced in the README (v0.7.3). Fabián Silva's **Caramelo** VS Code extension demonstrated a visual UI approach to SDD. [\[github.com\]](https://github.com/github/spec-kit)
## SDD Ecosystem & Industry Trends
### The "Spec Layer" Debate
Matt Rickard's "The Spec Layer" essay established a new framing for SDD: specifications as **constraint surfaces** that reduce execution freedom for AI agents. His comparison of six SDD tools argued for smaller, more focused specs with harder verification checks — a departure from comprehensive specification documents. This framing resonated across the community, with the Thoughtworks Radar entry and multiple comparison articles echoing the tension between spec depth and practical overhead.
### Competitive Landscape
**Will Torber's** three-framework comparison (Spec Kit, BMAD, OpenSpec) recommended **OpenSpec for most teams**, citing lower ceremony and better brownfield support. **Truong Phung** proposed combining Spec Kit with **Superpowers** (Jesse Vincent) for a "plan WHAT + control HOW" hybrid. These comparisons reflected a maturing market where practitioners combine tools rather than picking one.
The **Thoughtworks Radar** placement validated SDD as a category worth tracking but flagged instruction bloat and context rot as open concerns — the same issues the Augment Code comparison raised in March. XB Software's field report confirmed these in practice: SDD adds value for complex legacy work but creates unnecessary overhead for small tasks.
Spec Kit continued to lead in **GitHub popularity** (92k stars) and **agent breadth** (29 integrations). The market continued to differentiate along several axes: Spec Kit on portability and ecosystem breadth, Intent on living specs and drift detection, BMAD-METHOD on multi-agent orchestration, and OpenSpec on simplicity. [\[dev.to\]](https://dev.to/willtorber/spec-kit-vs-bmad-vs-openspec-choosing-an-sdd-framework-in-2026-d3j) [\[thoughtworks.com\]](https://www.thoughtworks.com/radar/languages-and-frameworks/github-spec-kit)
## Roadmap
Areas under discussion or in progress for future development:
- **Spec lifecycle management** — context rot and spec drift remained the most cited concern across articles (Thoughtworks Radar, XB Software, Will Torber). The marker-based upsert (v0.7.3) addressed context file drift; spec-level drift detection remains an open area. The Reconcile and Archive extensions are community steps toward this. [\[thoughtworks.com\]](https://www.thoughtworks.com/radar/languages-and-frameworks/github-spec-kit)
- **Workflow customization** — the workflow engine (v0.7.0) and preset composition strategies (v0.8.0) provide the foundation. Community presets for fiction writing, screenwriting, Jira tracking, and architecture governance demonstrate the breadth of possible workflows beyond standard SDD. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Catalog discovery and distribution** — the integration catalog (v0.7.2) and catalog discovery CLI (v0.8.3) bring `specify` closer to a package-manager experience for extensions, presets, and integrations. Private catalog authentication (v0.8.2) supports enterprise distribution. [\[github.com\]](https://github.com/github/spec-kit/releases)
- **Experience simplification** — the bundled lean preset (v0.6.1), `specify self check` (v0.7.5), and the deprecation of `--ai` in favor of `--integration` (v0.7.1) reflect ongoing work to reduce ceremony and improve the onboarding experience. Multiple external articles (Torber, XB Software) noted SDD overhead as a barrier. [\[dev.to\]](https://dev.to/willtorber/spec-kit-vs-bmad-vs-openspec-choosing-an-sdd-framework-in-2026-d3j)
- **Cross-platform and enterprise** — Windows CI (v0.7.1), GITHUB_TOKEN authentication (v0.8.2), Salesforce-specific extensions, and the iSAQB architecture governance preset indicate growing enterprise adoption. [\[github.com\]](https://github.com/github/spec-kit)

View File

@@ -41,6 +41,24 @@ The resolution is implemented three times to ensure consistency:
- **Bash**: `resolve_template()` in `scripts/bash/common.sh`
- **PowerShell**: `Resolve-Template` in `scripts/powershell/common.ps1`
### Composition Strategies
Templates, commands, and scripts support a `strategy` field that controls how a preset's content is combined with lower-priority content instead of fully replacing it:
| Strategy | Description | Templates | Commands | Scripts |
|----------|-------------|-----------|----------|---------|
| `replace` (default) | Fully replaces lower-priority content | ✓ | ✓ | ✓ |
| `prepend` | Places content before lower-priority content (separated by a blank line) | ✓ | ✓ | — |
| `append` | Places content after lower-priority content (separated by a blank line) | ✓ | ✓ | — |
| `wrap` | Content contains `{CORE_TEMPLATE}` (templates/commands) or `$CORE_SCRIPT` (scripts) placeholder replaced with lower-priority content | ✓ | ✓ | ✓ |
Composition is recursive — multiple composing presets chain. The `PresetResolver.resolve_content()` method walks the full priority stack bottom-up and applies each layer's strategy.
Content resolution functions for composition:
- **Python**: `PresetResolver.resolve_content()` in `src/specify_cli/presets.py` (templates, commands, and scripts)
- **Bash**: `resolve_template_content()` in `scripts/bash/common.sh` (templates only; command/script composition is handled by the Python resolver)
- **PowerShell**: `Resolve-TemplateContent` in `scripts/powershell/common.ps1` (templates only; command/script composition is handled by the Python resolver)
## Command Registration
When a preset is installed with `type: "command"` entries, the `PresetManager` registers them into all detected agent directories using the shared `CommandRegistrar` from `src/specify_cli/agents.py`.

View File

@@ -205,11 +205,21 @@ Edit `presets/catalog.community.json` and add your preset.
}
```
### 3. Submit Pull Request
### 3. Update Community Presets Table
Add your preset to the Community Presets table on the docs site at `docs/community/presets.md`:
```markdown
| Your Preset Name | Brief description of what your preset does | N templates, M commands[, P scripts] | — | [repo-name](https://github.com/your-org/spec-kit-preset-your-preset) |
```
Insert your row in alphabetical order by preset **name** (the first column of the table).
### 4. Submit Pull Request
```bash
git checkout -b add-your-preset
git add presets/catalog.community.json
git add presets/catalog.community.json docs/community/presets.md
git commit -m "Add your-preset to community catalog
- Preset ID: your-preset
@@ -240,6 +250,7 @@ git push origin add-your-preset
- [ ] Commands register to agent directories (if applicable)
- [ ] Commands match template sections (command + template are coherent)
- [ ] Added to presets/catalog.community.json
- [ ] Added row to docs/community/presets.md table
```
---

View File

@@ -61,14 +61,44 @@ specify preset add healthcare-compliance --priority 5 # overrides enterprise-sa
specify preset add pm-workflow --priority 1 # overrides everything
```
Presets **override**, they don't merge. If two presets both provide `spec-template`, the one with the lowest priority number wins entirely.
Presets **override by default**, they don't merge. If two presets both provide `spec-template` with the default `replace` strategy, the one with the lowest priority number wins entirely. However, presets can use **composition strategies** to augment rather than replace content.
### Composition Strategies
Presets can declare a `strategy` per template to control how content is combined. The `name` field identifies which template to compose with in the priority stack, while `file` points to the actual content file (which can differ from the convention path `templates/<name>.md`):
```yaml
provides:
templates:
- type: "template"
name: "spec-template"
file: "templates/spec-addendum.md"
strategy: "append" # adds content after the core template
```
| Strategy | Description |
|----------|-------------|
| `replace` (default) | Fully replaces the lower-priority template |
| `prepend` | Places content **before** the resolved lower-priority template, separated by a blank line |
| `append` | Places content **after** the resolved lower-priority template, separated by a blank line |
| `wrap` | Content contains `{CORE_TEMPLATE}` placeholder (or `$CORE_SCRIPT` for scripts) replaced with the lower-priority content |
**Supported combinations:**
| Type | `replace` | `prepend` | `append` | `wrap` |
|------|-----------|-----------|----------|--------|
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
| **script** | ✓ (default) | — | — | ✓ |
Multiple composing presets chain recursively. For example, a security preset with `prepend` and a compliance preset with `append` will produce: security header + core content + compliance footer.
## Catalog Management
Presets are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
> [!NOTE]
> Community presets are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting, catalog structure, or policy compliance, but they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
> Community presets are independently created and maintained by their respective authors. Maintainers only verify that catalog entries are complete and correctly formatted — they do **not review, audit, endorse, or support the preset code itself**. Review preset source code before installation and use at your own discretion.
```bash
# List active catalogs
@@ -93,9 +123,25 @@ See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset
## Environment Variables
| Variable | Description |
|----------|-------------|
| `SPECKIT_PRESET_CATALOG_URL` | Override the catalog URL (replaces all defaults) |
| Variable | Description | Default |
|----------|-------------|---------|
| `SPECKIT_PRESET_CATALOG_URL` | Override the full catalog stack with a single URL (replaces all defaults) | Built-in default stack |
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token for authenticated requests to GitHub-hosted URLs (`raw.githubusercontent.com`, `github.com`, `api.github.com`, `codeload.github.com`). Required when your catalog JSON or preset ZIPs are hosted in a private GitHub repository. | None |
#### Example: Using a private GitHub-hosted catalog
```bash
# Authenticate with a token (gh CLI, PAT, or GITHUB_TOKEN in CI)
export GITHUB_TOKEN=$(gh auth token)
# Search a private catalog added via `specify preset catalog add`
specify preset search my-template
# Install from a private catalog
specify preset add my-template
```
The token is attached automatically to requests targeting GitHub domains. Non-GitHub catalog URLs are always fetched without credentials.
## Configuration Files
@@ -108,13 +154,5 @@ See [scaffold/](scaffold/) for a scaffold you can copy to create your own preset
The following enhancements are under consideration for future releases:
- **Composition strategies** — Allow presets to declare a `strategy` per template instead of the default `replace`:
| Type | `replace` | `prepend` | `append` | `wrap` |
|------|-----------|-----------|----------|--------|
| **template** | ✓ (default) | ✓ | ✓ | ✓ |
| **command** | ✓ (default) | ✓ | ✓ | ✓ |
| **script** | ✓ (default) | — | — | ✓ |
For artifacts and commands (which are LLM directives), `wrap` would inject preset content before and after the core template using a `{CORE_TEMPLATE}` placeholder. For scripts, `wrap` would run custom logic before/after the core script via a `$CORE_SCRIPT` variable.
- **Script overrides** — Enable presets to provide alternative versions of core scripts (e.g. `create-new-feature.sh`) for workflow customization. A `strategy: "wrap"` option could allow presets to run custom logic before/after the core script without fully replacing it.
- **Structural merge strategies** — Parsing Markdown sections for per-section granularity (e.g., "replace only ## Security").
- **Conflict detection** — `specify preset lint` / `specify preset doctor` for detecting composition conflicts.

View File

@@ -1,8 +1,64 @@
{
"schema_version": "1.0",
"updated_at": "2026-04-13T00:00:00Z",
"updated_at": "2026-05-05T10:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
"presets": {
"a11y-governance": {
"name": "A11Y Governance",
"id": "a11y-governance",
"version": "0.2.0",
"description": "Adds accessibility, bilingual DE/EN delivery, CEFR-B2 readability, and inclusive-content governance to Spec Kit.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-a11y-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-a11y-governance/archive/refs/tags/v0.2.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-a11y-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-a11y-governance/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 9,
"commands": 3
},
"tags": [
"a11y",
"accessibility",
"bilingual",
"wcag",
"inclusion"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-04-27T00:00:00Z"
},
"agent-parity-governance": {
"name": "Agent Parity Governance",
"id": "agent-parity-governance",
"version": "0.1.0",
"description": "Keeps shared AI-agent guidance aligned across a project-defined set of agent instruction surfaces.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/archive/refs/tags/v0.1.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-agent-parity-governance/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 6,
"commands": 3
},
"tags": [
"agents",
"governance",
"parity",
"agent-guidance",
"multi-agent"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-04-27T00:00:00Z"
},
"aide-in-place": {
"name": "AIDE In-Place Migration",
"id": "aide-in-place",
@@ -16,7 +72,9 @@
"license": "MIT",
"requires": {
"speckit_version": ">=0.2.0",
"extensions": ["aide"]
"extensions": [
"aide"
]
},
"provides": {
"templates": 2,
@@ -29,6 +87,34 @@
"aide"
]
},
"architecture-governance": {
"name": "Architecture Governance",
"id": "architecture-governance",
"version": "0.2.0",
"description": "Adds secure architecture governance, threat modeling, STRIDE/CAPEC, Zero Trust, S-ADRs, and OWASP SAMM to Spec Kit.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-architecture-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-architecture-governance/archive/refs/tags/v0.2.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-architecture-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-architecture-governance/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 11,
"commands": 3
},
"tags": [
"architecture",
"governance",
"threat-modeling",
"stride",
"zero-trust"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-04-27T00:00:00Z"
},
"canon-core": {
"name": "Canon Core",
"id": "canon-core",
@@ -80,6 +166,34 @@
"created_at": "2026-04-13T00:00:00Z",
"updated_at": "2026-04-13T00:00:00Z"
},
"cross-platform-governance": {
"name": "Cross-Platform Governance",
"id": "cross-platform-governance",
"version": "0.1.0",
"description": "Adds Bash and PowerShell parity, dry-run/WhatIf parity, man-page expectations, and Verb-Noun Cmdlet discipline.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/archive/refs/tags/v0.1.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 8,
"commands": 3
},
"tags": [
"cross-platform",
"bash",
"powershell",
"man-page",
"cmdlet"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-04-27T00:00:00Z"
},
"explicit-task-dependencies": {
"name": "Explicit Task Dependencies",
"id": "explicit-task-dependencies",
@@ -108,11 +222,11 @@
"fiction-book-writing": {
"name": "Fiction Book Writing",
"id": "fiction-book-writing",
"version": "1.6.0",
"description": "Spec-Driven Development for novel and long-form fiction. 27 AI commands from idea to submission: story bible governance, 9 POV modes, all major plot structure frameworks, scene-by-scene drafting with quality gates, audiobook pipeline (SSML/ElevenLabs), cover design, sensitivity review, pacing and prose statistics, and pandoc-based export to DOCX/EPUB/LaTeX. Two style modes: author voice sample extraction or humanized-AI prose with 5 craft profiles. 12 languages supported.",
"version": "1.7.0",
"description": "Spec-Driven Development for novel and long-form fiction. 27 AI commands from idea to submission: story bible governance, 9 POV modes, all major plot structure frameworks, scene-by-scene drafting with quality gates, audiobook pipeline (SSML/ElevenLabs), cover design, sensitivity review, pacing and prose statistics, and pandoc-based export to DOCX/EPUB/LaTeX. Two style modes: author voice sample extraction or humanized-AI prose with 5 craft profiles. 12 languages supported. Support for offline semantic search.",
"author": "Andreas Daumann",
"repository": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
"download_url": "https://github.com/adaumann/speckit-preset-fiction-book-writing/archive/refs/tags/v1.6.0.zip",
"download_url": "https://github.com/adaumann/speckit-preset-fiction-book-writing/archive/refs/tags/v1.7.0.zip",
"homepage": "https://github.com/adaumann/speckit-preset-fiction-book-writing",
"documentation": "https://github.com/adaumann/speckit-preset-fiction-book-writing/blob/main/fiction-book-writing/README.md",
"license": "MIT",
@@ -122,7 +236,7 @@
"provides": {
"templates": 22,
"commands": 27,
"scripts": 1
"scripts": 2
},
"tags": [
"writing",
@@ -140,8 +254,63 @@
"language-support"
],
"created_at": "2026-04-09T08:00:00Z",
"updated_at": "2026-04-19T08:00:00Z"
},
"updated_at": "2026-04-27T08:00:00Z"
},
"isaqb-architecture-governance": {
"name": "iSAQB Architecture Governance",
"id": "isaqb-architecture-governance",
"version": "0.1.0",
"description": "Adds general iSAQB/CPSA-F and arc42 architecture governance, including views, quality scenarios, ADRs, risks, and technical debt.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/archive/refs/tags/v0.1.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 13,
"commands": 3
},
"tags": [
"architecture",
"governance",
"isaqb",
"arc42",
"adr"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-04-27T00:00:00Z"
},
"jira": {
"name": "Jira Issue Tracking",
"id": "jira",
"version": "1.0.0",
"description": "Overrides speckit.taskstoissues to create Jira epics, stories, and tasks instead of GitHub Issues via Atlassian MCP tools.",
"author": "luno",
"repository": "https://github.com/luno/spec-kit-preset-jira",
"download_url": "https://github.com/luno/spec-kit-preset-jira/archive/refs/tags/v1.0.0.zip",
"homepage": "https://github.com/luno/spec-kit-preset-jira",
"documentation": "https://github.com/luno/spec-kit-preset-jira/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"templates": 0,
"commands": 1
},
"tags": [
"jira",
"atlassian",
"issue-tracking",
"preset"
],
"created_at": "2026-04-15T00:00:00Z",
"updated_at": "2026-04-15T00:00:00Z"
},
"multi-repo-branching": {
"name": "Multi-Repo Branching",
"id": "multi-repo-branching",
@@ -194,6 +363,99 @@
"experimental"
]
},
"screenwriting": {
"name": "Screenwriting",
"id": "screenwriting",
"version": "1.0.0",
"description": "Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Spec Kit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents replace prose fiction conventions. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks.",
"author": "Andreas Daumann",
"repository": "https://github.com/adaumann/speckit-preset-screenwriting",
"download_url": "https://github.com/adaumann/speckit-preset-screenwriting/archive/refs/tags/v1.0.0.zip",
"homepage": "https://github.com/adaumann/speckit-preset-screenwriting",
"documentation": "https://github.com/adaumann/speckit-preset-screenwriting/blob/main/screenwriting/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.5.0"
},
"provides": {
"templates": 26,
"commands": 32,
"scripts": 1
},
"tags": [
"writing",
"screenplay",
"scriptwriting",
"film",
"tv",
"fountain",
"fountain-format",
"beat-sheet",
"teleplay",
"drama",
"comedy",
"storytelling",
"tutorial",
"education"
],
"created_at": "2026-04-23T08:00:00Z",
"updated_at": "2026-04-23T08:00:00Z"
},
"security-governance": {
"name": "Security Governance",
"id": "security-governance",
"version": "0.2.0",
"description": "Adds secure development governance, MSL preference, ASVS verification, supply-chain transparency, and EU CRA awareness.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-security-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.2.0.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-security-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-security-governance/blob/main/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 12,
"commands": 3
},
"tags": [
"security",
"governance",
"msl",
"asvs",
"supply-chain"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-04-27T00:00:00Z"
},
"spec2cloud": {
"name": "Spec2Cloud",
"id": "spec2cloud",
"version": "1.1.0",
"description": "Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy.",
"author": "Azure Samples",
"repository": "https://github.com/Azure-Samples/Spec2Cloud",
"download_url": "https://github.com/Azure-Samples/Spec2Cloud/releases/download/spec-kit-spec2cloud-v1.1.0/preset.zip",
"homepage": "https://aka.ms/spec2cloud",
"documentation": "https://github.com/Azure-Samples/Spec2Cloud/blob/main/spec-kit/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.1.0"
},
"provides": {
"templates": 5,
"commands": 8
},
"tags": [
"azure",
"spec2cloud",
"workflow",
"deployment"
],
"created_at": "2026-04-30T00:00:00Z",
"updated_at": "2026-04-30T00:00:00Z"
},
"toc-navigation": {
"name": "Table of Contents Navigation",
"id": "toc-navigation",

View File

@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-04-10T00:00:00Z",
"updated_at": "2026-04-24T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.json",
"presets": {
"lean": {
@@ -10,7 +10,15 @@
"description": "Minimal core workflow commands - just the prompt, just the artifact",
"author": "github",
"repository": "https://github.com/github/spec-kit",
"license": "MIT",
"bundled": true,
"requires": {
"speckit_version": ">=0.6.0"
},
"provides": {
"commands": 5,
"templates": 0
},
"tags": [
"lean",
"minimal",

45
presets/lean/README.md Normal file
View File

@@ -0,0 +1,45 @@
# Lean Workflow
A minimal preset that strips the Spec Kit workflow down to its essentials — just the prompt, just the artifact.
## When to Use
Use Lean when you want the structured specify → plan → tasks → implement pipeline without the ceremony of the full templates. Each command produces a single focused Markdown file with no boilerplate sections to fill in.
## Commands Included
| Command | Output | Description |
|---------|--------|-------------|
| `speckit.specify` | `spec.md` | Create a specification from a feature description |
| `speckit.plan` | `plan.md` | Create an implementation plan from the spec |
| `speckit.tasks` | `tasks.md` | Create dependency-ordered tasks from spec and plan |
| `speckit.implement` | *(code)* | Execute all tasks in order, marking progress |
| `speckit.constitution` | `constitution.md` | Create or update the project constitution |
## What It Replaces
Lean overrides the five core workflow commands with self-contained prompts that produce each artifact directly — no separate template files involved. The result is a shorter, more direct workflow.
## Installation
```bash
# Lean is a bundled preset — no download needed
specify preset add lean
```
## Development
```bash
# Test from local directory
specify preset add --dev ./presets/lean
# Verify commands resolve
specify preset resolve speckit.specify
# Remove when done
specify preset remove lean
```
## License
MIT

View File

@@ -48,3 +48,4 @@ tags:
- "lean"
- "minimal"
- "workflow"
- "core"

View File

@@ -32,6 +32,15 @@ provides:
templates:
# CUSTOMIZE: Define your template overrides
# Templates are document scaffolds (spec-template.md, plan-template.md, etc.)
#
# Strategy options (optional, defaults to "replace"):
# replace - Fully replaces the lower-priority template (default)
# prepend - Places this content BEFORE the lower-priority template
# append - Places this content AFTER the lower-priority template
# wrap - Uses {CORE_TEMPLATE} placeholder (templates/commands) or
# $CORE_SCRIPT placeholder (scripts), replaced with lower-priority content
#
# Note: Scripts only support "replace" and "wrap" strategies.
- type: "template"
name: "spec-template"
file: "templates/spec-template.md"
@@ -45,6 +54,26 @@ provides:
# description: "Custom plan template"
# replaces: "plan-template"
# COMPOSITION EXAMPLES:
# The `file` field points to the content file (can differ from the
# convention path `templates/<name>.md`). The `name` field identifies
# which template to compose with in the priority stack.
#
# Append additional sections to an existing template:
# - type: "template"
# name: "spec-template"
# file: "templates/spec-addendum.md"
# description: "Add compliance section to spec template"
# strategy: "append"
#
# Wrap a command with preamble/sign-off:
# - type: "command"
# name: "speckit.specify"
# file: "commands/specify-wrapper.md"
# description: "Wrap specify command with compliance checks"
# strategy: "wrap"
# # In the wrapper file, use {CORE_TEMPLATE} where the original content goes
# OVERRIDE EXTENSION TEMPLATES:
# Presets sit above extensions in the resolution stack, so you can
# override templates provided by any installed extension.

View File

@@ -0,0 +1,14 @@
---
description: "Self-test wrap command — pre/post around core"
strategy: wrap
---
## Preset Pre-Logic
preset:self-test wrap-pre
{CORE_TEMPLATE}
## Preset Post-Logic
preset:self-test wrap-post

View File

@@ -56,6 +56,11 @@ provides:
description: "Self-test override of the specify command"
replaces: "speckit.specify"
- type: "command"
name: "speckit.wrap-test"
file: "commands/speckit.wrap-test.md"
description: "Self-test wrap strategy command"
tags:
- "testing"
- "self-test"

View File

@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.7.4"
version = "0.8.6"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
requires-python = ">=3.11"
dependencies = [

View File

@@ -153,6 +153,59 @@ check_feature_branch() {
return 0
}
# Safely read .specify/feature.json's "feature_directory" value.
# Prints the raw value (possibly relative) to stdout, or empty string if the file
# is missing, unparseable, or does not contain the key. Always returns 0 so callers
# under `set -e` cannot be aborted by parser failure.
# Parser order mirrors the historical get_feature_paths behavior: jq -> python3 -> grep/sed.
read_feature_json_feature_directory() {
local repo_root="$1"
local fj="$repo_root/.specify/feature.json"
[[ -f "$fj" ]] || { printf '%s' ''; return 0; }
local _fd=''
if command -v jq >/dev/null 2>&1; then
if ! _fd=$(jq -r '.feature_directory // empty' "$fj" 2>/dev/null); then
_fd=''
fi
elif command -v python3 >/dev/null 2>&1; then
# Use Python so pretty-printed/multi-line JSON still parses correctly.
if ! _fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); v=d.get('feature_directory'); print(v if v else '')" "$fj" 2>/dev/null); then
_fd=''
fi
else
# Last-resort single-line grep/sed fallback. The `|| true` guards against
# grep returning 1 (no match) aborting under `set -e` / `pipefail`.
_fd=$( { grep -E '"feature_directory"[[:space:]]*:' "$fj" 2>/dev/null || true; } \
| head -n 1 \
| sed -E 's/^[^:]*:[[:space:]]*"([^"]*)".*$/\1/' )
fi
printf '%s' "$_fd"
return 0
}
# Returns 0 when .specify/feature.json lists feature_directory that exists as a directory
# and matches the resolved active FEATURE_DIR (so /speckit.plan can skip git branch pattern checks).
# Delegates parsing to read_feature_json_feature_directory, which is safe under `set -e`.
feature_json_matches_feature_dir() {
local repo_root="$1"
local active_feature_dir="$2"
local _fd
_fd=$(read_feature_json_feature_directory "$repo_root")
[[ -n "$_fd" ]] || return 1
[[ "$_fd" != /* ]] && _fd="$repo_root/$_fd"
[[ -d "$_fd" ]] || return 1
local norm_json norm_active
norm_json="$(cd -- "$_fd" 2>/dev/null && pwd -P)" || return 1
norm_active="$(cd -- "$active_feature_dir" 2>/dev/null && pwd -P)" || return 1
[[ "$norm_json" == "$norm_active" ]]
}
# Find feature directory by numeric prefix instead of exact branch match
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
find_feature_dir_by_prefix() {
@@ -217,16 +270,10 @@ get_feature_paths() {
# Normalize relative paths to absolute under repo root
[[ "$feature_dir" != /* ]] && feature_dir="$repo_root/$feature_dir"
elif [[ -f "$repo_root/.specify/feature.json" ]]; then
# Shared, set -e-safe parser: jq -> python3 -> grep/sed. Returns empty on
# missing/unparseable/unset so we fall through to the branch-prefix lookup.
local _fd
if command -v jq >/dev/null 2>&1; then
_fd=$(jq -r '.feature_directory // empty' "$repo_root/.specify/feature.json" 2>/dev/null)
elif command -v python3 >/dev/null 2>&1; then
# Fallback: use Python to parse JSON so pretty-printed/multi-line files work
_fd=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('feature_directory',''))" "$repo_root/.specify/feature.json" 2>/dev/null)
else
# Last resort: single-line grep fallback (won't work on multi-line JSON)
_fd=$(grep -o '"feature_directory"[[:space:]]*:[[:space:]]*"[^"]*"' "$repo_root/.specify/feature.json" 2>/dev/null | sed 's/.*"\([^"]*\)"$/\1/')
fi
_fd=$(read_feature_json_feature_directory "$repo_root")
if [[ -n "$_fd" ]]; then
feature_dir="$_fd"
# Normalize relative paths to absolute under repo root
@@ -320,8 +367,9 @@ try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10)):
print(pid)
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
@@ -373,3 +421,225 @@ except Exception:
return 1
}
# Resolve a template name to composed content using composition strategies.
# Reads strategy metadata from preset manifests and composes content
# from multiple layers using prepend, append, or wrap strategies.
#
# Usage: CONTENT=$(resolve_template_content "template-name" "$REPO_ROOT")
# Returns composed content string on stdout; exit code 1 if not found.
resolve_template_content() {
local template_name="$1"
local repo_root="$2"
local base="$repo_root/.specify/templates"
# Collect all layers (highest priority first)
local -a layer_paths=()
local -a layer_strategies=()
# Priority 1: Project overrides (always "replace")
local override="$base/overrides/${template_name}.md"
if [ -f "$override" ]; then
layer_paths+=("$override")
layer_strategies+=("replace")
fi
# Priority 2: Installed presets (sorted by priority from .registry)
local presets_dir="$repo_root/.specify/presets"
if [ -d "$presets_dir" ]; then
local registry_file="$presets_dir/.registry"
local sorted_presets=""
if [ -f "$registry_file" ] && command -v python3 >/dev/null 2>&1; then
if sorted_presets=$(SPECKIT_REGISTRY="$registry_file" python3 -c "
import json, sys, os
try:
with open(os.environ['SPECKIT_REGISTRY']) as f:
data = json.load(f)
presets = data.get('presets', {})
for pid, meta in sorted(presets.items(), key=lambda x: x[1].get('priority', 10) if isinstance(x[1], dict) else 10):
if isinstance(meta, dict) and meta.get('enabled', True) is not False:
print(pid)
except Exception:
sys.exit(1)
" 2>/dev/null); then
if [ -n "$sorted_presets" ]; then
local yaml_warned=false
while IFS= read -r preset_id; do
# Read strategy and file path from preset manifest
local strategy="replace"
local manifest_file=""
local manifest="$presets_dir/$preset_id/preset.yml"
if [ -f "$manifest" ] && command -v python3 >/dev/null 2>&1; then
# Requires PyYAML; falls back to replace/convention if unavailable
local result
local py_stderr
py_stderr=$(mktemp)
result=$(SPECKIT_MANIFEST="$manifest" SPECKIT_TMPL="$template_name" python3 -c "
import sys, os
try:
import yaml
except ImportError:
print('yaml_missing', file=sys.stderr)
print('replace\t')
sys.exit(0)
try:
with open(os.environ['SPECKIT_MANIFEST']) as f:
data = yaml.safe_load(f)
for t in data.get('provides', {}).get('templates', []):
if t.get('name') == os.environ['SPECKIT_TMPL'] and t.get('type', 'template') == 'template':
print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
sys.exit(0)
print('replace\t')
except Exception:
print('replace\t')
" 2>"$py_stderr")
local parse_status=$?
if [ $parse_status -eq 0 ] && [ -n "$result" ]; then
IFS=$'\t' read -r strategy manifest_file <<< "$result"
strategy=$(printf '%s' "$strategy" | tr '[:upper:]' '[:lower:]')
fi
if [ "$yaml_warned" = false ] && grep -q 'yaml_missing' "$py_stderr" 2>/dev/null; then
echo "Warning: PyYAML not available; composition strategies may be ignored" >&2
yaml_warned=true
fi
rm -f "$py_stderr"
fi
# Try manifest file path first, then convention path
local candidate=""
if [ -n "$manifest_file" ]; then
# Reject absolute paths and parent traversal
case "$manifest_file" in
/*|*../*|../*) manifest_file="" ;;
esac
fi
if [ -n "$manifest_file" ]; then
local mf="$presets_dir/$preset_id/$manifest_file"
[ -f "$mf" ] && candidate="$mf"
fi
if [ -z "$candidate" ]; then
local cf="$presets_dir/$preset_id/templates/${template_name}.md"
[ -f "$cf" ] && candidate="$cf"
fi
if [ -n "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("$strategy")
fi
done <<< "$sorted_presets"
fi
else
# python3 failed — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
else
# No python3 or registry — fall back to unordered directory scan (replace only)
for preset in "$presets_dir"/*/; do
[ -d "$preset" ] || continue
local candidate="$preset/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
fi
# Priority 3: Extension-provided templates (always "replace")
local ext_dir="$repo_root/.specify/extensions"
if [ -d "$ext_dir" ]; then
for ext in "$ext_dir"/*/; do
[ -d "$ext" ] || continue
case "$(basename "$ext")" in .*) continue;; esac
local candidate="$ext/templates/${template_name}.md"
if [ -f "$candidate" ]; then
layer_paths+=("$candidate")
layer_strategies+=("replace")
fi
done
fi
# Priority 4: Core templates (always "replace")
local core="$base/${template_name}.md"
if [ -f "$core" ]; then
layer_paths+=("$core")
layer_strategies+=("replace")
fi
local count=${#layer_paths[@]}
[ "$count" -eq 0 ] && return 1
# Check if any layer uses a non-replace strategy
local has_composition=false
for s in "${layer_strategies[@]}"; do
[ "$s" != "replace" ] && has_composition=true && break
done
# If the top (highest-priority) layer is replace, it wins entirely —
# lower layers are irrelevant regardless of their strategies.
if [ "${layer_strategies[0]}" = "replace" ]; then
cat "${layer_paths[0]}"
return 0
fi
if [ "$has_composition" = false ]; then
cat "${layer_paths[0]}"
return 0
fi
# Find the effective base: scan from highest priority (index 0) downward
# to find the nearest replace layer. Only compose layers above that base.
local base_idx=-1
local i
for (( i=0; i<count; i++ )); do
if [ "${layer_strategies[$i]}" = "replace" ]; then
base_idx=$i
break
fi
done
if [ $base_idx -lt 0 ]; then
return 1 # no base layer found
fi
# Read the base content; compose layers above the base (higher priority)
local content
content=$(cat "${layer_paths[$base_idx]}"; printf x)
content="${content%x}"
for (( i=base_idx-1; i>=0; i-- )); do
local path="${layer_paths[$i]}"
local strat="${layer_strategies[$i]}"
local layer_content
# Preserve trailing newlines
layer_content=$(cat "$path"; printf x)
layer_content="${layer_content%x}"
case "$strat" in
replace) content="$layer_content" ;;
prepend) content="$(printf '%s\n\n%s' "$layer_content" "$content")" ;;
append) content="$(printf '%s\n\n%s' "$content" "$layer_content")" ;;
wrap)
case "$layer_content" in
*'{CORE_TEMPLATE}'*) ;;
*) echo "Error: wrap strategy missing {CORE_TEMPLATE} placeholder" >&2; return 1 ;;
esac
while [[ "$layer_content" == *'{CORE_TEMPLATE}'* ]]; do
local before="${layer_content%%\{CORE_TEMPLATE\}*}"
local after="${layer_content#*\{CORE_TEMPLATE\}}"
layer_content="${before}${content}${after}"
done
content="$layer_content"
;;
*) echo "Error: unknown strategy '$strat'" >&2; return 1 ;;
esac
done
printf '%s' "$content"
return 0
}

View File

@@ -84,7 +84,7 @@ if [ -z "$FEATURE_DESCRIPTION" ]; then
fi
# Trim whitespace and validate description is not empty (e.g., user passed only whitespace)
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | xargs)
FEATURE_DESCRIPTION=$(echo "$FEATURE_DESCRIPTION" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')
if [ -z "$FEATURE_DESCRIPTION" ]; then
echo "Error: Feature description cannot be empty or contain only whitespace" >&2
exit 1

View File

@@ -32,8 +32,10 @@ _paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature p
eval "$_paths_output"
unset _paths_output
# Check if we're on a proper feature branch (only for git repos)
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
# If feature.json pins an existing feature directory, branch naming is not required.
if ! feature_json_matches_feature_dir "$REPO_ROOT" "$FEATURE_DIR"; then
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
fi
# Ensure the feature directory exists
mkdir -p "$FEATURE_DIR"

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -e
# Parse command line arguments
JSON_MODE=false
for arg in "$@"; do
case "$arg" in
--json) JSON_MODE=true ;;
--help|-h)
echo "Usage: $0 [--json]"
echo " --json Output results in JSON format"
echo " --help Show this help message"
exit 0
;;
*) echo "ERROR: Unknown option '$arg'" >&2; exit 1 ;;
esac
done
# Source common functions
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/common.sh"
# Get feature paths
_paths_output=$(get_feature_paths) || { echo "ERROR: Failed to resolve feature paths" >&2; exit 1; }
eval "$_paths_output"
unset _paths_output
# Validate branch
# If feature.json pins an existing feature directory, branch naming is not required.
if ! feature_json_matches_feature_dir "$REPO_ROOT" "$FEATURE_DIR"; then
check_feature_branch "$CURRENT_BRANCH" "$HAS_GIT" || exit 1
fi
if [[ ! -f "$IMPL_PLAN" ]]; then
echo "ERROR: plan.md not found in $FEATURE_DIR" >&2
echo "Run /speckit.plan first to create the implementation plan." >&2
exit 1
fi
if [[ ! -f "$FEATURE_SPEC" ]]; then
echo "ERROR: spec.md not found in $FEATURE_DIR" >&2
echo "Run /speckit.specify first to create the feature structure." >&2
exit 1
fi
# Build available docs list
docs=()
[[ -f "$RESEARCH" ]] && docs+=("research.md")
[[ -f "$DATA_MODEL" ]] && docs+=("data-model.md")
if [[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]; then
docs+=("contracts/")
fi
[[ -f "$QUICKSTART" ]] && docs+=("quickstart.md")
# Resolve tasks template through override stack
TASKS_TEMPLATE=$(resolve_template "tasks-template" "$REPO_ROOT") || true
if [[ -z "$TASKS_TEMPLATE" ]] || [[ ! -f "$TASKS_TEMPLATE" ]]; then
echo "ERROR: Could not resolve required tasks-template from the template override stack for $REPO_ROOT" >&2
echo "Template 'tasks-template' was not found in any supported location (overrides, presets, extensions, or shared core). Add an override at .specify/templates/overrides/tasks-template.md, or run 'specify init' / reinstall shared infra to restore the core .specify/templates/tasks-template.md template." >&2
exit 1
fi
# Output results
if $JSON_MODE; then
if has_jq; then
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(printf '%s\n' "${docs[@]}" | jq -R . | jq -s .)
fi
jq -cn \
--arg feature_dir "$FEATURE_DIR" \
--argjson docs "$json_docs" \
--arg tasks_template "${TASKS_TEMPLATE:-}" \
'{FEATURE_DIR:$feature_dir,AVAILABLE_DOCS:$docs,TASKS_TEMPLATE:$tasks_template}'
else
if [[ ${#docs[@]} -eq 0 ]]; then
json_docs="[]"
else
json_docs=$(for d in "${docs[@]}"; do printf '"%s",' "$(json_escape "$d")"; done)
json_docs="[${json_docs%,}]"
fi
printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s,"TASKS_TEMPLATE":"%s"}\n' \
"$(json_escape "$FEATURE_DIR")" "$json_docs" "$(json_escape "${TASKS_TEMPLATE:-}")"
fi
else
echo "FEATURE_DIR: $FEATURE_DIR"
echo "TASKS_TEMPLATE: ${TASKS_TEMPLATE:-not found}"
echo "AVAILABLE_DOCS:"
check_file "$RESEARCH" "research.md"
check_file "$DATA_MODEL" "data-model.md"
check_dir "$CONTRACTS_DIR" "contracts/"
check_file "$QUICKSTART" "quickstart.md"
fi

View File

@@ -164,6 +164,74 @@ function Test-FeatureBranch {
return $true
}
# True when .specify/feature.json pins an existing feature directory that matches the
# active FEATURE_DIR from Get-FeaturePathsEnv (so /speckit.plan can skip git branch pattern checks).
function Test-FeatureJsonMatchesFeatureDir {
param(
[Parameter(Mandatory = $true)][string]$RepoRoot,
[Parameter(Mandatory = $true)][string]$ActiveFeatureDir
)
$featureJson = Join-Path (Join-Path $RepoRoot '.specify') 'feature.json'
if (-not (Test-Path -LiteralPath $featureJson -PathType Leaf)) {
return $false
}
try {
$raw = Get-Content -LiteralPath $featureJson -Raw
$cfg = $raw | ConvertFrom-Json
} catch {
return $false
}
$fd = $cfg.feature_directory
if ([string]::IsNullOrWhiteSpace([string]$fd)) {
return $false
}
if (-not [System.IO.Path]::IsPathRooted($fd)) {
$fd = Join-Path $RepoRoot $fd
}
if (-not (Test-Path -LiteralPath $fd -PathType Container)) {
return $false
}
# Resolve both paths to canonical absolute form. Prefer Resolve-Path (follows
# symlinks and is the canonical PS way); fall back to [Path]::GetFullPath when
# Resolve-Path can't produce a value. Mirrors the pattern used by Find-SpecifyRoot.
$resolvedJson = Resolve-Path -LiteralPath $fd -ErrorAction SilentlyContinue
if ($resolvedJson) {
$normJson = $resolvedJson.Path
} else {
$normJson = [System.IO.Path]::GetFullPath($fd)
}
$resolvedActive = Resolve-Path -LiteralPath $ActiveFeatureDir -ErrorAction SilentlyContinue
if ($resolvedActive) {
$normActive = $resolvedActive.Path
} else {
$normActive = [System.IO.Path]::GetFullPath($ActiveFeatureDir)
}
# Use case-insensitive compare only on Windows; POSIX filesystems are case-sensitive.
# PowerShell 5.1 is Windows-only and does not define $IsWindows, so treat its
# absence as "we're on Windows".
if ($null -ne $IsWindows) {
$onWindows = $IsWindows
} else {
$onWindows = $true
}
if ($onWindows) {
$comparison = [System.StringComparison]::OrdinalIgnoreCase
} else {
$comparison = [System.StringComparison]::Ordinal
}
return [string]::Equals($normJson, $normActive, $comparison)
}
# Resolve specs/<feature-dir> by numeric/timestamp prefix (mirrors scripts/bash/common.sh find_feature_dir_by_prefix).
function Find-FeatureDirByPrefix {
param(
@@ -287,6 +355,21 @@ function Test-DirHasFiles {
}
}
# Find a usable Python 3 executable (python3, python, or py -3).
# Returns the command/arguments as an array, or $null if none found.
function Get-Python3Command {
if (Get-Command python3 -ErrorAction SilentlyContinue) { return @('python3') }
if (Get-Command python -ErrorAction SilentlyContinue) {
$ver = & python --version 2>&1
if ($ver -match 'Python 3') { return @('python') }
}
if (Get-Command py -ErrorAction SilentlyContinue) {
$ver = & py -3 --version 2>&1
if ($ver -match 'Python 3') { return @('py', '-3') }
}
return $null
}
# Resolve a template name to a file path using the priority stack:
# 1. .specify/templates/overrides/
# 2. .specify/presets/<preset-id>/templates/ (sorted by priority from .registry)
@@ -315,6 +398,7 @@ function Resolve-Template {
$presets = $registryData.presets
if ($presets) {
$sortedPresets = $presets.PSObject.Properties |
Where-Object { $null -eq $_.Value.enabled -or $_.Value.enabled -ne $false } |
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
ForEach-Object { $_.Name }
}
@@ -354,3 +438,206 @@ function Resolve-Template {
return $null
}
# Resolve a template name to composed content using composition strategies.
# Reads strategy metadata from preset manifests and composes content
# from multiple layers using prepend, append, or wrap strategies.
function Resolve-TemplateContent {
param(
[Parameter(Mandatory=$true)][string]$TemplateName,
[Parameter(Mandatory=$true)][string]$RepoRoot
)
$base = Join-Path $RepoRoot '.specify/templates'
# Collect all layers (highest priority first)
$layerPaths = @()
$layerStrategies = @()
# Priority 1: Project overrides (always "replace")
$override = Join-Path $base "overrides/$TemplateName.md"
if (Test-Path $override) {
$layerPaths += $override
$layerStrategies += 'replace'
}
# Priority 2: Installed presets (sorted by priority from .registry)
$presetsDir = Join-Path $RepoRoot '.specify/presets'
if (Test-Path $presetsDir) {
$registryFile = Join-Path $presetsDir '.registry'
$sortedPresets = @()
if (Test-Path $registryFile) {
try {
$registryData = Get-Content $registryFile -Raw | ConvertFrom-Json
$presets = $registryData.presets
if ($presets) {
$sortedPresets = $presets.PSObject.Properties |
Where-Object { $null -eq $_.Value.enabled -or $_.Value.enabled -ne $false } |
Sort-Object { if ($null -ne $_.Value.priority) { $_.Value.priority } else { 10 } } |
ForEach-Object { $_.Name }
}
} catch {
$sortedPresets = @()
}
}
if ($sortedPresets.Count -gt 0) {
$pyCmd = Get-Python3Command
if (-not $pyCmd) {
# Check if any preset has strategy fields that would be ignored
foreach ($pid in $sortedPresets) {
$mf = Join-Path $presetsDir "$pid/preset.yml"
if ((Test-Path $mf) -and (Select-String -Path $mf -Pattern 'strategy:' -Quiet -ErrorAction SilentlyContinue)) {
Write-Warning "No Python 3 found; preset composition strategies will be ignored"
break
}
}
}
$yamlWarned = $false
foreach ($presetId in $sortedPresets) {
# Read strategy and file path from preset manifest
$strategy = 'replace'
$manifestFilePath = ''
$manifest = Join-Path $presetsDir "$presetId/preset.yml"
if ((Test-Path $manifest) -and $pyCmd) {
try {
# Use Python to parse YAML manifest for strategy and file path
$pyArgs = if ($pyCmd.Count -gt 1) { $pyCmd[1..($pyCmd.Count-1)] } else { @() }
$pyStderrFile = [System.IO.Path]::GetTempFileName()
$stratResult = & $pyCmd[0] @pyArgs -c @"
import sys
try:
import yaml
except ImportError:
print('yaml_missing', file=sys.stderr)
print('replace\t')
sys.exit(0)
try:
with open(sys.argv[1]) as f:
data = yaml.safe_load(f)
for t in data.get('provides', {}).get('templates', []):
if t.get('name') == sys.argv[2] and t.get('type', 'template') == 'template':
print(t.get('strategy', 'replace') + '\t' + t.get('file', ''))
sys.exit(0)
print('replace\t')
except Exception:
print('replace\t')
"@ $manifest $TemplateName 2>$pyStderrFile
if ($stratResult) {
$parts = $stratResult.Trim() -split "`t", 2
$strategy = $parts[0].ToLowerInvariant()
if ($parts.Count -gt 1 -and $parts[1]) { $manifestFilePath = $parts[1] }
}
if (-not $yamlWarned -and (Test-Path $pyStderrFile) -and (Get-Content $pyStderrFile -Raw -ErrorAction SilentlyContinue) -match 'yaml_missing') {
Write-Warning "PyYAML not available; composition strategies may be ignored"
$yamlWarned = $true
}
Remove-Item $pyStderrFile -Force -ErrorAction SilentlyContinue
} catch {
$strategy = 'replace'
if ($pyStderrFile) { Remove-Item $pyStderrFile -Force -ErrorAction SilentlyContinue }
}
}
# Try manifest file path first, then convention path
$candidate = $null
if ($manifestFilePath) {
# Reject absolute paths and parent traversal
if ([System.IO.Path]::IsPathRooted($manifestFilePath) -or $manifestFilePath -match '\.\.[\\/]') {
$manifestFilePath = ''
}
}
if ($manifestFilePath) {
$mf = Join-Path $presetsDir "$presetId/$manifestFilePath"
if (Test-Path $mf) { $candidate = $mf }
}
if (-not $candidate) {
$cf = Join-Path $presetsDir "$presetId/templates/$TemplateName.md"
if (Test-Path $cf) { $candidate = $cf }
}
if ($candidate) {
$layerPaths += $candidate
$layerStrategies += $strategy
}
}
} else {
# Fallback: alphabetical directory order (no registry or parse failure)
foreach ($preset in Get-ChildItem -Path $presetsDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' }) {
$candidate = Join-Path $preset.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) {
$layerPaths += $candidate
$layerStrategies += 'replace'
}
}
}
}
# Priority 3: Extension-provided templates (always "replace")
$extDir = Join-Path $RepoRoot '.specify/extensions'
if (Test-Path $extDir) {
foreach ($ext in Get-ChildItem -Path $extDir -Directory -ErrorAction SilentlyContinue | Where-Object { $_.Name -notlike '.*' } | Sort-Object Name) {
$candidate = Join-Path $ext.FullName "templates/$TemplateName.md"
if (Test-Path $candidate) {
$layerPaths += $candidate
$layerStrategies += 'replace'
}
}
}
# Priority 4: Core templates (always "replace")
$core = Join-Path $base "$TemplateName.md"
if (Test-Path $core) {
$layerPaths += $core
$layerStrategies += 'replace'
}
if ($layerPaths.Count -eq 0) { return $null }
# If the top (highest-priority) layer is replace, it wins entirely —
# lower layers are irrelevant regardless of their strategies.
if ($layerStrategies[0] -eq 'replace') {
return (Get-Content $layerPaths[0] -Raw)
}
# Check if any layer uses a non-replace strategy
$hasComposition = $false
foreach ($s in $layerStrategies) {
if ($s -ne 'replace') { $hasComposition = $true; break }
}
if (-not $hasComposition) {
return (Get-Content $layerPaths[0] -Raw)
}
# Find the effective base: scan from highest priority (index 0) downward
# to find the nearest replace layer. Only compose layers above that base.
$baseIdx = -1
for ($i = 0; $i -lt $layerPaths.Count; $i++) {
if ($layerStrategies[$i] -eq 'replace') {
$baseIdx = $i
break
}
}
if ($baseIdx -lt 0) { return $null }
$content = Get-Content $layerPaths[$baseIdx] -Raw
for ($i = $baseIdx - 1; $i -ge 0; $i--) {
$path = $layerPaths[$i]
$strat = $layerStrategies[$i]
$layerContent = Get-Content $path -Raw
switch ($strat) {
'replace' { $content = $layerContent }
'prepend' { $content = "$layerContent`n`n$content" }
'append' { $content = "$content`n`n$layerContent" }
'wrap' {
if (-not $layerContent.Contains('{CORE_TEMPLATE}')) {
throw "Wrap strategy missing {CORE_TEMPLATE} placeholder"
}
$content = $layerContent.Replace('{CORE_TEMPLATE}', $content)
}
default { throw "Unknown strategy: $strat" }
}
}
return $content
}

View File

@@ -23,9 +23,11 @@ if ($Help) {
# Get all paths and variables from common functions
$paths = Get-FeaturePathsEnv
# Check if we're on a proper feature branch (only for git repos)
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
exit 1
# If feature.json pins an existing feature directory, branch naming is not required.
if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFeatureDir $paths.FEATURE_DIR)) {
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
exit 1
}
}
# Ensure the feature directory exists

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param(
[switch]$Json,
[switch]$Help
)
$ErrorActionPreference = 'Stop'
if ($Help) {
Write-Output "Usage: setup-tasks.ps1 [-Json] [-Help]"
exit 0
}
# Source common functions
. "$PSScriptRoot/common.ps1"
# Get feature paths and validate branch
$paths = Get-FeaturePathsEnv
# If feature.json pins an existing feature directory, branch naming is not required.
if (-not (Test-FeatureJsonMatchesFeatureDir -RepoRoot $paths.REPO_ROOT -ActiveFeatureDir $paths.FEATURE_DIR)) {
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
exit 1
}
}
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
[Console]::Error.WriteLine("ERROR: plan.md not found in $($paths.FEATURE_DIR)")
[Console]::Error.WriteLine("Run /speckit.plan first to create the implementation plan.")
exit 1
}
if (-not (Test-Path $paths.FEATURE_SPEC -PathType Leaf)) {
[Console]::Error.WriteLine("ERROR: spec.md not found in $($paths.FEATURE_DIR)")
[Console]::Error.WriteLine("Run /speckit.specify first to create the feature structure.")
exit 1
}
# Build available docs list
$docs = @()
if (Test-Path $paths.RESEARCH) { $docs += 'research.md' }
if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' }
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) {
$docs += 'contracts/'
}
if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' }
# Resolve tasks template through override stack
$tasksTemplate = Resolve-Template -TemplateName 'tasks-template' -RepoRoot $paths.REPO_ROOT
if (-not $tasksTemplate -or -not (Test-Path -LiteralPath $tasksTemplate -PathType Leaf)) {
$expectedCoreTemplate = Join-Path $paths.REPO_ROOT '.specify/templates/tasks-template.md'
[Console]::Error.WriteLine("ERROR: Tasks template not found for repository root: $($paths.REPO_ROOT)`nTemplate resolution order: overrides -> presets -> extensions -> core.`nExpected shared/core template location: $expectedCoreTemplate`nTo continue, verify whether 'tasks-template.md' is available in '.specify/templates/overrides/', preset templates, extension templates, or restore the shared/core templates (for example by re-running 'specify init') so that '.specify/templates/tasks-template.md' exists.")
exit 1
}
$tasksTemplate = (Resolve-Path -LiteralPath $tasksTemplate).Path
# Output results
if ($Json) {
[PSCustomObject]@{
FEATURE_DIR = $paths.FEATURE_DIR
AVAILABLE_DOCS = $docs
TASKS_TEMPLATE = $tasksTemplate
} | ConvertTo-Json -Compress
} else {
Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
Write-Output "TASKS_TEMPLATE: $(if ($tasksTemplate) { $tasksTemplate } else { 'not found' })"
Write-Output "AVAILABLE_DOCS:"
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
"""Shared GitHub-authenticated HTTP helpers.
Used by both ExtensionCatalog and PresetCatalog to attach
GITHUB_TOKEN / GH_TOKEN credentials to requests targeting
GitHub-hosted domains, while preventing token leakage to
third-party hosts on redirects.
"""
import os
import urllib.request
from typing import Dict
from urllib.parse import urlparse
# GitHub-owned hostnames that should receive the Authorization header.
# Includes codeload.github.com because GitHub archive URL downloads
# (e.g. /archive/refs/tags/<tag>.zip) redirect there and require auth
# for private repositories.
GITHUB_HOSTS = frozenset({
"raw.githubusercontent.com",
"github.com",
"api.github.com",
"codeload.github.com",
})
def build_github_request(url: str) -> urllib.request.Request:
"""Build a urllib Request, adding a GitHub auth header when available.
Reads GITHUB_TOKEN or GH_TOKEN from the environment and attaches an
``Authorization: Bearer <value>`` header when the target hostname is one
of the known GitHub-owned domains. Non-GitHub URLs are returned as plain
requests so credentials are never leaked to third-party hosts.
Raises:
ValueError: If ``url`` is empty or whitespace-only.
ValueError: If ``url`` does not use the ``http`` or ``https`` scheme.
ValueError: If ``url`` does not include a hostname.
"""
headers: Dict[str, str] = {}
url = url.strip()
if not url:
raise ValueError("url must not be empty")
parsed = urlparse(url)
if parsed.scheme not in {"http", "https"}:
raise ValueError(f"url must start with http:// or https://, got: {url!r}")
if not parsed.hostname:
raise ValueError(f"url must include a hostname, got: {url!r}")
github_token = (os.environ.get("GITHUB_TOKEN") or "").strip()
gh_token = (os.environ.get("GH_TOKEN") or "").strip()
token = github_token or gh_token or None
hostname = parsed.hostname.lower()
if token and hostname in GITHUB_HOSTS:
headers["Authorization"] = f"Bearer {token}"
return urllib.request.Request(url, headers=headers)
class _StripAuthOnRedirect(urllib.request.HTTPRedirectHandler):
"""Redirect handler that drops the Authorization header when leaving GitHub.
Prevents token leakage to CDNs or other third-party hosts that GitHub
may redirect to (e.g. S3 for release asset downloads, objects.githubusercontent.com).
Auth is preserved as long as the redirect target remains within GITHUB_HOSTS.
"""
def redirect_request(self, req, fp, code, msg, headers, newurl):
original_auth = req.get_header("Authorization")
new_req = super().redirect_request(req, fp, code, msg, headers, newurl)
if new_req is not None:
hostname = (urlparse(newurl).hostname or "").lower()
if hostname in GITHUB_HOSTS:
if original_auth:
new_req.add_unredirected_header("Authorization", original_auth)
else:
new_req.headers.pop("Authorization", None)
new_req.unredirected_hdrs.pop("Authorization", None)
return new_req
def open_github_url(url: str, timeout: int = 10):
"""Open a URL with GitHub auth, stripping the header on cross-host redirects.
When the request carries an Authorization header, a custom redirect
handler drops that header if the redirect target is not a GitHub-owned
domain, preventing token leakage to CDNs or other third-party hosts
that GitHub may redirect to (e.g. S3 for release asset downloads).
"""
req = build_github_request(url)
if not req.get_header("Authorization"):
return urllib.request.urlopen(req, timeout=timeout)
opener = urllib.request.build_opener(_StripAuthOnRedirect)
return opener.open(req, timeout=timeout)

View File

@@ -6,8 +6,9 @@ Used by both the extension system and the preset system to write
command files into agent-specific directories in the correct format.
"""
import os
from pathlib import Path
from typing import Dict, List, Any
from typing import Dict, List, Any, Optional
import platform
import re
@@ -281,7 +282,8 @@ class CommandRegistrar:
if not isinstance(frontmatter, dict):
frontmatter = {}
if agent_name in {"codex", "kimi"}:
agent_config = self.AGENT_CONFIGS.get(agent_name, {})
if agent_config.get("extension") == "/SKILL.md":
body = self.resolve_skill_placeholders(
agent_name, frontmatter, body, project_root
)
@@ -399,6 +401,28 @@ class CommandRegistrar:
return f"speckit-{short_name}"
@staticmethod
def _ensure_inside(candidate: Path, base: Path) -> None:
"""Validate that a write target stays within the expected base directory.
Uses lexical normalization so traversal via ``..`` or absolute paths is
rejected while intentionally symlinked sub-directories remain
supported.
Args:
candidate: Path that will be written.
base: Directory the write must remain within.
Raises:
ValueError: If the normalized candidate path escapes ``base``.
"""
normalized = Path(os.path.normpath(candidate))
base_normalized = Path(os.path.normpath(base))
if not normalized.is_relative_to(base_normalized):
raise ValueError(
f"Output path {candidate!r} escapes directory {base!r}"
)
def register_commands(
self,
agent_name: str,
@@ -445,6 +469,15 @@ class CommandRegistrar:
content = source_file.read_text(encoding="utf-8")
frontmatter, body = self.parse_frontmatter(content)
if frontmatter.get("strategy") == "wrap":
from .presets import _substitute_core_template
body, core_frontmatter = _substitute_core_template(body, cmd_name, project_root, self)
frontmatter = dict(frontmatter)
for key in ("scripts", "agent_scripts"):
if key not in frontmatter and key in core_frontmatter:
frontmatter[key] = core_frontmatter[key]
frontmatter.pop("strategy", None)
frontmatter = self._adjust_script_paths(frontmatter)
for key in agent_config.get("strip_frontmatter_keys", []):
@@ -472,10 +505,12 @@ class CommandRegistrar:
project_root,
)
elif agent_config["format"] == "markdown":
output = self.render_markdown_command(
frontmatter, body, source_id, context_note
)
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
body = self._convert_argument_placeholder(body, "$ARGUMENTS", agent_config["args"])
output = self.render_markdown_command(frontmatter, body, source_id, context_note)
elif agent_config["format"] == "toml":
body = self.resolve_skill_placeholders(agent_name, frontmatter, body, project_root)
body = self._convert_argument_placeholder(body, "$ARGUMENTS", agent_config["args"])
output = self.render_toml_command(frontmatter, body, source_id)
elif agent_config["format"] == "yaml":
output = self.render_yaml_command(
@@ -485,6 +520,7 @@ class CommandRegistrar:
raise ValueError(f"Unsupported format: {agent_config['format']}")
dest_file = commands_dir / f"{output_name}{agent_config['extension']}"
self._ensure_inside(dest_file, commands_dir)
dest_file.parent.mkdir(parents=True, exist_ok=True)
dest_file.write_text(output, encoding="utf-8")
@@ -550,12 +586,7 @@ class CommandRegistrar:
alias_file = (
commands_dir / f"{alias_output_name}{agent_config['extension']}"
)
try:
alias_file.resolve().relative_to(commands_dir.resolve())
except ValueError:
raise ValueError(
f"Alias output path escapes commands directory: {alias_file!r}"
)
self._ensure_inside(alias_file, commands_dir)
alias_file.parent.mkdir(parents=True, exist_ok=True)
alias_file.write_text(alias_output, encoding="utf-8")
if agent_name == "copilot":
@@ -575,6 +606,7 @@ class CommandRegistrar:
prompts_dir = project_root / ".github" / "prompts"
prompts_dir.mkdir(parents=True, exist_ok=True)
prompt_file = prompts_dir / f"{cmd_name}.prompt.md"
CommandRegistrar._ensure_inside(prompt_file, prompts_dir)
prompt_file.write_text(f"---\nagent: {cmd_name}\n---\n", encoding="utf-8")
def register_commands_for_all_agents(
@@ -620,6 +652,49 @@ class CommandRegistrar:
return results
def register_commands_for_non_skill_agents(
self,
commands: List[Dict[str, Any]],
source_id: str,
source_dir: Path,
project_root: Path,
context_note: Optional[str] = None,
) -> Dict[str, List[str]]:
"""Register commands for all non-skill agents in the project.
Like register_commands_for_all_agents but skips skill-based agents
(those with extension '/SKILL.md'). Used by reconciliation to avoid
overwriting properly formatted SKILL.md files.
Args:
commands: List of command info dicts
source_id: Identifier of the source
source_dir: Directory containing command source files
project_root: Path to project root
context_note: Custom context comment for markdown output
Returns:
Dictionary mapping agent names to list of registered commands
"""
results = {}
self._ensure_configs()
for agent_name, agent_config in self.AGENT_CONFIGS.items():
if agent_config.get("extension") == "/SKILL.md":
continue
agent_dir = project_root / agent_config["dir"]
if agent_dir.exists():
try:
registered = self.register_commands(
agent_name, commands, source_id,
source_dir, project_root,
context_note=context_note,
)
if registered:
results[agent_name] = registered
except ValueError:
continue
return results
def unregister_commands(
self, registered_commands: Dict[str, List[str]], project_root: Path
) -> None:

View File

@@ -139,12 +139,23 @@ class ExtensionManifest:
def _load_yaml(self, path: Path) -> dict:
"""Load YAML file safely."""
try:
with open(path, 'r') as f:
return yaml.safe_load(f) or {}
with open(path, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
except yaml.YAMLError as e:
raise ValidationError(f"Invalid YAML in {path}: {e}")
except FileNotFoundError:
raise ValidationError(f"Manifest not found: {path}")
except UnicodeDecodeError as e:
raise ValidationError(
f"Manifest is not valid UTF-8: {path} ({e.reason} at byte {e.start})"
)
except OSError as e:
raise ValidationError(f"Could not read manifest {path}: {e}")
if not isinstance(data, dict):
raise ValidationError(
f"Manifest must be a YAML mapping, got {type(data).__name__}: {path}"
)
return data
def _validate(self):
"""Validate manifest structure and required fields."""
@@ -951,29 +962,40 @@ class ExtensionManager:
return written
def _unregister_extension_skills(self, skill_names: List[str], extension_id: str) -> None:
def _unregister_extension_skills(
self,
skill_names: List[str],
extension_id: str,
skills_dir: Optional[Path] = None,
) -> None:
"""Remove SKILL.md directories for extension skills.
Called during extension removal to clean up skill files that
were created by ``_register_extension_skills()``.
If ``_get_skills_dir()`` returns ``None`` (e.g. the user removed
init-options.json or toggled ai_skills after installation), we
fall back to scanning all known agent skills directories so that
orphaned skill directories are still cleaned up. In that case
each candidate directory is verified against the SKILL.md
``metadata.source`` field before removal to avoid accidentally
deleting user-created skills with the same name.
If *skills_dir* is not provided and ``_get_skills_dir()`` returns
``None`` (e.g. the user removed init-options.json or toggled
ai_skills after installation), we fall back to scanning all known
agent skills directories so that orphaned skill directories are
still cleaned up. In that case each candidate directory is
verified against the SKILL.md ``metadata.source`` field before
removal to avoid accidentally deleting user-created skills with
the same name.
Args:
skill_names: List of skill names to remove.
extension_id: Extension ID used to verify ownership during
fallback candidate scanning.
skills_dir: Optional explicit skills directory to use instead
of resolving via ``_get_skills_dir()``. Useful when the
caller needs to target a specific agent's skills directory
regardless of the currently-active agent in init-options.
"""
if not skill_names:
return
skills_dir = self._get_skills_dir()
if skills_dir is None:
skills_dir = self._get_skills_dir()
if skills_dir:
# Fast path: we know the exact skills directory
@@ -1097,7 +1119,7 @@ class ExtensionManager:
raise CompatibilityError(
f"Extension requires spec-kit {required}, "
f"but {speckit_version} is installed.\n"
f"Upgrade spec-kit with: uv tool install specify-cli --force"
f"Upgrade spec-kit with: {REINSTALL_COMMAND}"
)
except InvalidSpecifier:
raise CompatibilityError(f"Invalid version specifier: {required}")
@@ -1321,6 +1343,156 @@ class ExtensionManager:
return True
@staticmethod
def _valid_name_list(value: Any) -> List[str]:
"""Return string entries from a registry list, ignoring corrupt values."""
if not isinstance(value, list):
return []
return [item for item in value if isinstance(item, str)]
def unregister_agent_artifacts(self, agent_name: str) -> None:
"""Remove extension files registered for a specific agent.
Extension command files are tracked per agent in ``registered_commands``.
Extension skills are scoped to the provided *agent_name*; they are removed
from that agent's skills directory (resolved via its integration config)
and the registry field is cleared.
Skips cleanup when *agent_name* is not a supported agent to avoid
losing registry entries while leaving orphaned files on disk.
"""
if not agent_name:
return
registrar = CommandRegistrar()
if agent_name not in registrar.AGENT_CONFIGS:
return
# Resolve the skills directory for the specific agent so cleanup is
# agent-scoped and does not depend on the currently-active agent in
# init-options. Use the same helper that extension install uses.
from . import _get_skills_dir as resolve_skills_dir
agent_skills_dir = resolve_skills_dir(self.project_root, agent_name)
for ext_id, metadata in self.registry.list().items():
updates: Dict[str, Any] = {}
registered_commands = metadata.get("registered_commands", {})
if isinstance(registered_commands, dict) and agent_name in registered_commands:
command_names = self._valid_name_list(registered_commands.get(agent_name))
if command_names:
registrar.unregister_commands({agent_name: command_names}, self.project_root)
new_registered = copy.deepcopy(registered_commands)
new_registered.pop(agent_name, None)
updates["registered_commands"] = new_registered
registered_skills = self._valid_name_list(metadata.get("registered_skills", []))
if registered_skills:
# Only pass the resolved skills_dir when it actually exists.
# Otherwise let _unregister_extension_skills fall back to
# scanning all known agent skills directories, which is useful
# for cleaning up stale entries created by earlier installs.
skills_dir = agent_skills_dir if agent_skills_dir.is_dir() else None
self._unregister_extension_skills(
registered_skills, ext_id, skills_dir=skills_dir
)
# Only reconcile registry state when cleanup was scoped to a
# specific existing directory. When skills_dir is None,
# _unregister_extension_skills falls back to scanning multiple
# candidate directories, so agent_skills_dir cannot be used to
# infer what was removed. When skills_dir is set,
# _unregister_extension_skills may intentionally skip deletion
# when ownership cannot be verified (e.g., corrupted/missing
# SKILL.md or mismatching metadata.source). Only drop registry
# entries for skill directories that were actually removed so
# future cleanup attempts can still find skipped ones.
if skills_dir is not None:
remaining_skills = [
skill_name
for skill_name in registered_skills
if (skills_dir / skill_name).is_dir()
]
if remaining_skills != registered_skills:
updates["registered_skills"] = remaining_skills
if updates:
self.registry.update(ext_id, updates)
def register_enabled_extensions_for_agent(self, agent_name: str) -> None:
"""Register installed, enabled extensions for ``agent_name``.
This is intended to be called after switching integrations. Command
registration is scoped to the explicit ``agent_name`` argument, but some
behavior still depends on the current init-options state (for example,
skills-mode handling uses the active ``ai`` / ``ai_skills`` settings).
Callers should therefore pass the agent that has just been made active
in init-options; in normal use, ``agent_name`` is expected to match the
current ``ai`` value. This mirrors extension install behavior while
avoiding stale default-mode command directories when that active agent
is running in skills mode (notably Copilot ``--skills``).
"""
if not agent_name:
return
from . import load_init_options
registrar = CommandRegistrar()
agent_config = registrar.AGENT_CONFIGS.get(agent_name)
init_options = load_init_options(self.project_root)
if not isinstance(init_options, dict):
init_options = {}
active_agent = init_options.get("ai")
skills_mode_active = (
active_agent == agent_name
and bool(init_options.get("ai_skills"))
and bool(agent_config)
and agent_config.get("extension") != "/SKILL.md"
)
for ext_id, metadata in self.registry.list().items():
if not metadata.get("enabled", True):
continue
manifest = self.get_extension(ext_id)
if manifest is None:
continue
ext_dir = self.extensions_dir / ext_id
updates: Dict[str, Any] = {}
if agent_config and not skills_mode_active:
registered = registrar.register_commands_for_agent(
agent_name, manifest, ext_dir, self.project_root
)
registered_commands = metadata.get("registered_commands", {})
if not isinstance(registered_commands, dict):
registered_commands = {}
new_registered = copy.deepcopy(registered_commands)
if registered:
new_registered[agent_name] = registered
else:
# Registration returned empty list (e.g., corrupted
# manifest pointing at missing command files). Clear
# stale entry so later cleanup doesn't try to remove
# files that were never written.
new_registered.pop(agent_name, None)
if new_registered != registered_commands:
updates["registered_commands"] = new_registered
registered_skills = self._register_extension_skills(manifest, ext_dir)
if registered_skills:
existing_skills = self._valid_name_list(metadata.get("registered_skills", []))
merged_skills = list(dict.fromkeys(existing_skills + registered_skills))
updates["registered_skills"] = merged_skills
if updates:
self.registry.update(ext_id, updates)
def list_installed(self) -> List[Dict[str, Any]]:
"""List all installed extensions with metadata.
@@ -1534,6 +1706,22 @@ class ExtensionCatalog:
if not parsed.netloc:
raise ValidationError("Catalog URL must be a valid URL with a host.")
def _make_request(self, url: str):
"""Build a urllib Request, adding a GitHub auth header when available.
Delegates to :func:`specify_cli._github_http.build_github_request`.
"""
from specify_cli._github_http import build_github_request
return build_github_request(url)
def _open_url(self, url: str, timeout: int = 10):
"""Open a URL with GitHub auth, stripping the header on cross-host redirects.
Delegates to :func:`specify_cli._github_http.open_github_url`.
"""
from specify_cli._github_http import open_github_url
return open_github_url(url, timeout)
def _load_catalog_config(self, config_path: Path) -> Optional[List[CatalogEntry]]:
"""Load catalog stack configuration from a YAML file.
@@ -1690,7 +1878,6 @@ class ExtensionCatalog:
Raises:
ExtensionError: If catalog cannot be fetched or has invalid format
"""
import urllib.request
import urllib.error
# Determine cache file paths (backward compat for default catalog)
@@ -1724,7 +1911,7 @@ class ExtensionCatalog:
# Fetch from network
try:
with urllib.request.urlopen(entry.url, timeout=10) as response:
with self._open_url(entry.url, timeout=10) as response:
catalog_data = json.loads(response.read())
if "schema_version" not in catalog_data or "extensions" not in catalog_data:
@@ -1838,10 +2025,9 @@ class ExtensionCatalog:
catalog_url = self.get_catalog_url()
try:
import urllib.request
import urllib.error
with urllib.request.urlopen(catalog_url, timeout=10) as response:
with self._open_url(catalog_url, timeout=10) as response:
catalog_data = json.loads(response.read())
# Validate catalog structure
@@ -1952,7 +2138,6 @@ class ExtensionCatalog:
Raises:
ExtensionError: If extension not found or download fails
"""
import urllib.request
import urllib.error
# Get extension info from catalog
@@ -1992,7 +2177,7 @@ class ExtensionCatalog:
# Download the ZIP file
try:
with urllib.request.urlopen(download_url, timeout=60) as response:
with self._open_url(download_url, timeout=60) as response:
zip_data = response.read()
zip_path.write_bytes(zip_data)

View File

@@ -0,0 +1,90 @@
"""Runtime helpers for integration commands."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from .integration_state import integration_setting, integration_settings
ParseOptions = Callable[[Any, str], dict[str, Any] | None]
def resolve_integration_options(
integration: Any,
state: dict[str, Any],
key: str,
raw_options: str | None,
*,
parse_options: ParseOptions,
) -> tuple[str | None, dict[str, Any] | None]:
"""Resolve raw and parsed options for an integration operation."""
if raw_options is not None:
return raw_options, parse_options(integration, raw_options)
setting = integration_setting(state, key)
stored_raw = setting.get("raw_options")
if not isinstance(stored_raw, str):
stored_raw = None
stored_parsed = setting.get("parsed_options")
if isinstance(stored_parsed, dict):
return stored_raw, stored_parsed or None
if stored_raw:
return stored_raw, parse_options(integration, stored_raw)
return None, None
def with_integration_setting(
state: dict[str, Any],
key: str,
integration: Any,
*,
script_type: str | None = None,
raw_options: str | None = None,
parsed_options: dict[str, Any] | None = None,
) -> dict[str, dict[str, Any]]:
"""Return integration settings with *key* updated."""
settings = integration_settings(state)
current = dict(settings.get(key, {}))
if script_type:
current["script"] = script_type
if raw_options is not None:
current["raw_options"] = raw_options
elif "raw_options" in current and not current.get("raw_options"):
current.pop("raw_options", None)
if parsed_options is not None:
current["parsed_options"] = parsed_options
elif raw_options is not None:
current.pop("parsed_options", None)
current["invoke_separator"] = integration.effective_invoke_separator(parsed_options)
settings[key] = current
return settings
def invoke_separator_for_integration(
integration: Any,
state: dict[str, Any],
key: str,
parsed_options: dict[str, Any] | None = None,
) -> str:
"""Resolve the invocation separator for stored/default integration state."""
if parsed_options is not None:
return integration.effective_invoke_separator(parsed_options)
setting = integration_setting(state, key)
stored_separator = setting.get("invoke_separator")
if isinstance(stored_separator, str) and stored_separator:
return stored_separator
stored_parsed = setting.get("parsed_options")
if isinstance(stored_parsed, dict):
return integration.effective_invoke_separator(stored_parsed)
return integration.effective_invoke_separator(None)

View File

@@ -0,0 +1,161 @@
"""State helpers for installed AI agent integrations."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
INTEGRATION_JSON = ".specify/integration.json"
INTEGRATION_STATE_SCHEMA = 1
def clean_integration_key(key: Any) -> str | None:
"""Return a stripped integration key, or None for empty/non-string values."""
if not isinstance(key, str) or not key.strip():
return None
return key.strip()
def dedupe_integration_keys(keys: list[Any]) -> list[str]:
"""Return a de-duplicated list of non-empty integration keys."""
seen: set[str] = set()
deduped: list[str] = []
for key in keys:
clean = clean_integration_key(key)
if clean is None:
continue
if clean in seen:
continue
seen.add(clean)
deduped.append(clean)
return deduped
def normalize_integration_settings(settings: Any) -> dict[str, dict[str, Any]]:
"""Return JSON-safe per-integration runtime settings."""
if not isinstance(settings, dict):
return {}
normalized: dict[str, dict[str, Any]] = {}
for key, value in settings.items():
if not isinstance(key, str) or not key.strip() or not isinstance(value, dict):
continue
clean: dict[str, Any] = {}
script = value.get("script")
if isinstance(script, str) and script.strip():
clean["script"] = script.strip()
raw_options = value.get("raw_options")
if isinstance(raw_options, str):
clean["raw_options"] = raw_options
parsed_options = value.get("parsed_options")
if isinstance(parsed_options, dict):
clean["parsed_options"] = parsed_options
invoke_separator = value.get("invoke_separator")
if isinstance(invoke_separator, str) and invoke_separator.strip():
clean["invoke_separator"] = invoke_separator.strip()
if clean:
normalized[key.strip()] = clean
return normalized
def _normalized_integration_state_schema(value: Any) -> int:
if isinstance(value, int) and not isinstance(value, bool) and value > INTEGRATION_STATE_SCHEMA:
return value
return INTEGRATION_STATE_SCHEMA
def normalize_integration_state(data: dict[str, Any]) -> dict[str, Any]:
"""Normalize legacy and multi-install integration metadata."""
legacy_key = clean_integration_key(data.get("integration"))
default_key = clean_integration_key(data.get("default_integration")) or legacy_key
installed = data.get("installed_integrations")
installed_keys = dedupe_integration_keys(installed if isinstance(installed, list) else [])
if not default_key and installed_keys:
default_key = installed_keys[0]
if default_key and default_key not in installed_keys:
installed_keys.insert(0, default_key)
settings = normalize_integration_settings(data.get("integration_settings"))
normalized = dict(data)
normalized["integration_state_schema"] = _normalized_integration_state_schema(
data.get("integration_state_schema")
)
if default_key:
normalized["integration"] = default_key
normalized["default_integration"] = default_key
else:
normalized.pop("integration", None)
normalized.pop("default_integration", None)
normalized["installed_integrations"] = installed_keys
normalized["integration_settings"] = {
key: settings[key] for key in installed_keys if key in settings
}
return normalized
def default_integration_key(state: dict[str, Any]) -> str | None:
"""Return the default integration key from normalized state."""
key = state.get("default_integration") or state.get("integration")
return clean_integration_key(key)
def installed_integration_keys(state: dict[str, Any]) -> list[str]:
"""Return installed integration keys from normalized state."""
return dedupe_integration_keys(state.get("installed_integrations", []))
def integration_settings(state: dict[str, Any]) -> dict[str, dict[str, Any]]:
"""Return normalized per-integration settings from state."""
return normalize_integration_settings(state.get("integration_settings"))
def integration_setting(state: dict[str, Any], key: str) -> dict[str, Any]:
"""Return stored runtime settings for *key*."""
return dict(integration_settings(state).get(key, {}))
def write_integration_json(
project_root: Path,
*,
version: str,
integration_key: str | None,
installed_integrations: list[str] | None = None,
settings: dict[str, dict[str, Any]] | None = None,
) -> None:
"""Write ``.specify/integration.json`` with legacy-compatible state."""
dest = project_root / INTEGRATION_JSON
dest.parent.mkdir(parents=True, exist_ok=True)
integration_key = clean_integration_key(integration_key)
installed = dedupe_integration_keys(installed_integrations or [])
if integration_key and integration_key not in installed:
installed.insert(0, integration_key)
if not integration_key and installed:
integration_key = installed[0]
normalized_settings = normalize_integration_settings(settings or {})
normalized_settings = {
key: normalized_settings[key] for key in installed if key in normalized_settings
}
data: dict[str, Any] = {
"version": version,
"integration_state_schema": INTEGRATION_STATE_SCHEMA,
"installed_integrations": installed,
"integration_settings": normalized_settings,
}
if integration_key:
data["integration"] = integration_key
data["default_integration"] = integration_key
dest.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")

View File

@@ -56,6 +56,7 @@ def _register_builtins() -> None:
from .codex import CodexIntegration
from .copilot import CopilotIntegration
from .cursor_agent import CursorAgentIntegration
from .devin import DevinIntegration
from .forge import ForgeIntegration
from .gemini import GeminiIntegration
from .generic import GenericIntegration
@@ -86,6 +87,7 @@ def _register_builtins() -> None:
_register(CodexIntegration())
_register(CopilotIntegration())
_register(CursorAgentIntegration())
_register(DevinIntegration())
_register(ForgeIntegration())
_register(GeminiIntegration())
_register(GenericIntegration())

View File

@@ -19,3 +19,4 @@ class AuggieIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = ".augment/rules/specify-rules.md"
multi_install_safe = True

View File

@@ -84,6 +84,17 @@ class IntegrationBase(ABC):
context_file: str | None = None
"""Relative path to the agent context file (e.g. ``CLAUDE.md``)."""
invoke_separator: str = "."
"""Separator used in slash-command invocations (``"."`` → ``/speckit.plan``)."""
multi_install_safe: bool = False
"""Whether this integration is declared safe to install alongside others.
Safe integrations must use a static, unique agent root, command directory,
and context file. Registry tests enforce those invariants for every
integration that sets this flag.
"""
# -- Markers for managed context section ------------------------------
CONTEXT_MARKER_START = "<!-- SPECKIT START -->"
@@ -96,6 +107,18 @@ class IntegrationBase(ABC):
"""Return options this integration accepts. Default: none."""
return []
def effective_invoke_separator(
self, parsed_options: dict[str, Any] | None = None
) -> str:
"""Return the invoke separator for the given options.
Subclasses whose separator depends on runtime options (e.g.
Copilot in ``--skills`` mode) should override this method.
The default implementation ignores *parsed_options* and returns
the class-level ``invoke_separator``.
"""
return self.invoke_separator
def build_exec_args(
self,
prompt: str,
@@ -122,11 +145,12 @@ class IntegrationBase(ABC):
agents or ``"/speckit-specify my-feature"`` for skills agents.
*command_name* may be a full dotted name like
``"speckit.specify"`` or a bare stem like ``"specify"``.
``"speckit.specify"``, an extension command like
``"speckit.git.commit"``, or a bare stem like ``"specify"``.
"""
stem = command_name
if "." in stem:
stem = stem.rsplit(".", 1)[-1]
if stem.startswith("speckit."):
stem = stem[len("speckit."):]
invocation = f"/speckit.{stem}"
if args:
@@ -597,6 +621,24 @@ class IntegrationBase(ABC):
return True
@staticmethod
def resolve_command_refs(content: str, separator: str = ".") -> str:
"""Replace ``__SPECKIT_COMMAND_<NAME>__`` placeholders with invocations.
Each placeholder encodes a command name in upper-case with
underscores (e.g. ``__SPECKIT_COMMAND_PLAN__``,
``__SPECKIT_COMMAND_GIT_COMMIT__``). The replacement uses
*separator* to join the segments:
* ``separator="."`` → ``/speckit.plan``, ``/speckit.git.commit``
* ``separator="-"`` → ``/speckit-plan``, ``/speckit-git-commit``
"""
return re.sub(
r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__",
lambda m: "/speckit" + separator + m.group(1).lower().replace("_", separator),
content,
)
@staticmethod
def process_template(
content: str,
@@ -604,6 +646,7 @@ class IntegrationBase(ABC):
script_type: str,
arg_placeholder: str = "$ARGUMENTS",
context_file: str = "",
invoke_separator: str = ".",
) -> str:
"""Process a raw command template into agent-ready content.
@@ -615,6 +658,7 @@ class IntegrationBase(ABC):
5. Replace ``__AGENT__`` with *agent_name*
6. Replace ``__CONTEXT_FILE__`` with *context_file*
7. Rewrite paths: ``scripts/`` → ``.specify/scripts/`` etc.
8. Replace ``__SPECKIT_COMMAND_<NAME>__`` with invocation strings
"""
# 1. Extract script command from frontmatter
script_command = ""
@@ -684,6 +728,9 @@ class IntegrationBase(ABC):
content = CommandRegistrar.rewrite_project_relative_paths(content)
# 8. Replace __SPECKIT_COMMAND_<NAME>__ with invocation strings
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
return content
def setup(
@@ -1274,6 +1321,8 @@ class SkillsIntegration(IntegrationBase):
``speckit-<name>/SKILL.md`` file with skills-oriented frontmatter.
"""
invoke_separator = "-"
def build_exec_args(
self,
prompt: str,
@@ -1311,10 +1360,10 @@ class SkillsIntegration(IntegrationBase):
def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Skills use ``/speckit-<stem>`` (hyphenated directory name)."""
stem = command_name
if "." in stem:
stem = stem.rsplit(".", 1)[-1]
if stem.startswith("speckit."):
stem = stem[len("speckit."):]
invocation = f"/speckit-{stem}"
invocation = "/speckit-" + stem.replace(".", "-")
if args:
invocation = f"{invocation} {args}"
return invocation
@@ -1395,6 +1444,7 @@ class SkillsIntegration(IntegrationBase):
processed_body = self.process_template(
raw, self.key, script_type, arg_placeholder,
context_file=self.context_file or "",
invoke_separator=self.invoke_separator,
)
# Strip the processed frontmatter — we rebuild it for skills.
# Preserve leading whitespace in the body to match release ZIP

View File

@@ -16,7 +16,7 @@ import re
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
import yaml
from packaging import version as pkg_version
@@ -30,6 +30,10 @@ class IntegrationCatalogError(Exception):
"""Raised when a catalog operation fails."""
class IntegrationValidationError(IntegrationCatalogError):
"""Validation error for catalog config or catalog management operations."""
class IntegrationDescriptorError(Exception):
"""Raised when an integration.yml descriptor is invalid."""
@@ -96,28 +100,36 @@ class IntegrationCatalog:
Returns None when the file does not exist.
Raises:
IntegrationCatalogError: on invalid content
IntegrationValidationError: on any local-config / YAML problem
(parse failures, wrong shape, missing/invalid fields,
invalid catalog URLs, etc.). This is a subclass of
:class:`IntegrationCatalogError`, so any caller that already
catches ``IntegrationCatalogError`` keeps working — but
callers that want to distinguish *local config* problems
from *remote/network* problems can match the subclass.
"""
if not config_path.exists():
return None
try:
data = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
except (yaml.YAMLError, OSError, UnicodeError) as exc:
raise IntegrationCatalogError(
raise IntegrationValidationError(
f"Failed to read catalog config {config_path}: {exc}"
)
) from exc
if data is None:
data = {}
if not isinstance(data, dict):
raise IntegrationCatalogError(
raise IntegrationValidationError(
f"Invalid catalog config {config_path}: expected a YAML mapping at the root"
)
catalogs_data = data.get("catalogs", [])
if not isinstance(catalogs_data, list):
raise IntegrationCatalogError(
f"Invalid catalog config: 'catalogs' must be a list, "
raise IntegrationValidationError(
f"Invalid catalog config {config_path}: 'catalogs' must be a list, "
f"got {type(catalogs_data).__name__}"
)
if not catalogs_data:
raise IntegrationCatalogError(
raise IntegrationValidationError(
f"Catalog config {config_path} exists but contains no 'catalogs' entries. "
f"Remove the file to use built-in defaults, or add valid catalog entries."
)
@@ -125,31 +137,52 @@ class IntegrationCatalog:
skipped: List[int] = []
for idx, item in enumerate(catalogs_data):
if not isinstance(item, dict):
raise IntegrationCatalogError(
f"Invalid catalog entry at index {idx}: "
raise IntegrationValidationError(
f"Invalid catalog config {config_path}: catalog entry at index {idx}: "
f"expected a mapping, got {type(item).__name__}"
)
url = str(item.get("url", "")).strip()
if not url:
skipped.append(idx)
continue
self._validate_catalog_url(url)
try:
priority = int(item.get("priority", idx + 1))
except (TypeError, ValueError):
raise IntegrationCatalogError(
self._validate_catalog_url(url)
except IntegrationCatalogError as exc:
# ``_validate_catalog_url`` raises the base class for direct
# callers (e.g. ``add_catalog`` validating user input); when
# the bad URL came from a local config file, surface it as a
# validation error so CLI handlers can route it accordingly.
raise IntegrationValidationError(
f"Invalid catalog URL in {config_path} at index {idx}: {exc}"
) from exc
raw_priority = item.get("priority", idx + 1)
if isinstance(raw_priority, bool):
raise IntegrationValidationError(
f"Invalid catalog config {config_path}: "
f"Invalid priority for catalog '{item.get('name', idx + 1)}': "
f"expected integer, got {item.get('priority')!r}"
f"expected integer, got {raw_priority!r}"
)
try:
priority = int(raw_priority)
except (TypeError, ValueError):
raise IntegrationValidationError(
f"Invalid catalog config {config_path}: "
f"Invalid priority for catalog '{item.get('name', idx + 1)}': "
f"expected integer, got {raw_priority!r}"
)
raw_install = item.get("install_allowed", False)
if isinstance(raw_install, str):
install_allowed = raw_install.strip().lower() in ("true", "yes", "1")
else:
install_allowed = bool(raw_install)
raw_name = item.get("name")
name = str(raw_name).strip() if raw_name is not None else ""
if not name:
name = f"catalog-{len(entries) + 1}"
entries.append(
IntegrationCatalogEntry(
url=url,
name=str(item.get("name", f"catalog-{idx + 1}")),
name=name,
priority=priority,
install_allowed=install_allowed,
description=str(item.get("description", "")),
@@ -157,7 +190,7 @@ class IntegrationCatalog:
)
entries.sort(key=lambda e: e.priority)
if not entries:
raise IntegrationCatalogError(
raise IntegrationValidationError(
f"Catalog config {config_path} contains {len(catalogs_data)} "
f"entries but none have valid URLs (entries at indices {skipped} "
f"were skipped). Each catalog entry must have a 'url' field."
@@ -196,12 +229,12 @@ class IntegrationCatalog:
)
]
project_cfg = self.project_root / ".specify" / "integration-catalogs.yml"
project_cfg = self.project_root / ".specify" / self.CONFIG_FILENAME
catalogs = self._load_catalog_config(project_cfg)
if catalogs is not None:
return catalogs
user_cfg = Path.home() / ".specify" / "integration-catalogs.yml"
user_cfg = Path.home() / ".specify" / self.CONFIG_FILENAME
catalogs = self._load_catalog_config(user_cfg)
if catalogs is not None:
return catalogs
@@ -408,6 +441,288 @@ class IntegrationCatalog:
for f in self.cache_dir.glob(pattern):
f.unlink(missing_ok=True)
# -- Catalog-source management ----------------------------------------
CONFIG_FILENAME = "integration-catalogs.yml"
def get_catalog_configs(self) -> List[Dict[str, Any]]:
"""Return the active catalog stack as a list of dicts.
Thin adapter over :meth:`get_active_catalogs` that yields plain dicts
suitable for CLI rendering and JSON-like consumers.
"""
return [
{
"name": e.name,
"url": e.url,
"priority": e.priority,
"install_allowed": e.install_allowed,
"description": e.description,
}
for e in self.get_active_catalogs()
]
def get_project_catalog_configs(self) -> Optional[List[Dict[str, Any]]]:
"""Return removable project-level catalog config entries, if configured."""
config_path = self.project_root / ".specify" / self.CONFIG_FILENAME
entries = self._load_catalog_config(config_path)
if entries is None:
return None
return [
{
"name": e.name,
"url": e.url,
"priority": e.priority,
"install_allowed": e.install_allowed,
"description": e.description,
}
for e in entries
]
def add_catalog(self, url: str, name: Optional[str] = None) -> None:
"""Add a catalog source to the project-level config file.
The URL is normalized (whitespace stripped) and validated before being
written. Duplicate URLs are rejected, including near-duplicates that
differ only by surrounding whitespace. Priority is derived as
``max(existing) + 1`` so the new entry sorts last in the resolution
order unless the user edits the file manually.
"""
url = url.strip()
if not url:
raise IntegrationValidationError("Catalog URL must be non-empty.")
self._validate_catalog_url(url)
config_path = self.project_root / ".specify" / self.CONFIG_FILENAME
data: Dict[str, Any] = {"catalogs": []}
if config_path.exists():
try:
raw = yaml.safe_load(config_path.read_text(encoding="utf-8"))
except (yaml.YAMLError, OSError, UnicodeError) as exc:
raise IntegrationValidationError(
f"Failed to read catalog config {config_path}: {exc}"
) from exc
if raw is None:
raw = {}
if not isinstance(raw, dict):
raise IntegrationValidationError(
f"Catalog config file {config_path} is corrupted "
"(expected a mapping)."
)
data = raw
catalogs = data.get("catalogs", [])
if not isinstance(catalogs, list):
raise IntegrationValidationError(
f"Catalog config {config_path} has invalid 'catalogs' value: "
"must be a list."
)
# Validate each existing entry before mutating anything. Fail fast so
# we don't silently preserve a corrupt sibling entry or derive a new
# priority from a bogus value.
existing_priorities: List[int] = []
valid_catalog_count = 0
for idx, cat in enumerate(catalogs):
if not isinstance(cat, dict):
raise IntegrationValidationError(
f"Invalid catalog entry at index {idx} in {config_path}: "
f"expected a mapping, got {type(cat).__name__}."
)
existing_url = str(cat.get("url", "")).strip()
if not existing_url:
continue
# Re-run the same URL validation used when loading, so a corrupt
# entry surfaces here instead of at the next `integration` call.
try:
self._validate_catalog_url(existing_url)
except IntegrationCatalogError as exc:
raise IntegrationValidationError(
f"Invalid catalog entry at index {idx} in {config_path}: {exc}"
) from exc
if existing_url == url:
raise IntegrationValidationError(
f"Catalog URL already configured: {url}"
)
valid_catalog_count += 1
if "priority" in cat:
raw_priority = cat.get("priority")
if isinstance(raw_priority, bool):
raise IntegrationValidationError(
f"Invalid catalog entry at index {idx} in {config_path}: "
f"'priority' must be an integer, got "
f"{type(raw_priority).__name__}."
)
try:
normalized_priority = int(raw_priority)
except (TypeError, ValueError):
raise IntegrationValidationError(
f"Invalid catalog entry at index {idx} in {config_path}: "
f"'priority' must be an integer, got "
f"{raw_priority!r}."
) from None
existing_priorities.append(normalized_priority)
else:
# Match `_load_catalog_config()`'s defaulting rule so the new
# entry still sorts after implicit-priority siblings.
existing_priorities.append(idx + 1)
max_priority = max(existing_priorities, default=0)
normalized_name = str(name).strip() if name is not None else ""
generated_name = f"catalog-{valid_catalog_count + 1}"
catalogs.append(
{
"name": normalized_name or generated_name,
"url": url,
"priority": max_priority + 1,
"install_allowed": True,
"description": "",
}
)
data["catalogs"] = catalogs
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, "w", encoding="utf-8") as f:
yaml.dump(
data,
f,
default_flow_style=False,
sort_keys=False,
allow_unicode=True,
)
def remove_catalog(self, index: int) -> str:
"""Remove a catalog source by 0-based index.
``index`` is interpreted in the same display order shown by
``integration catalog list`` (i.e. sorted ascending by priority,
with missing priority defaulting to ``yaml_index + 1``, matching
``_load_catalog_config()``). This way, the index a user sees in
``catalog list`` is the index they pass to ``catalog remove``,
even if the underlying YAML lists entries in a different order
from how they sort by priority.
Returns the removed catalog's name.
"""
config_path = self.project_root / ".specify" / self.CONFIG_FILENAME
if not config_path.exists():
raise IntegrationValidationError("No catalog config file found.")
try:
data = yaml.safe_load(config_path.read_text(encoding="utf-8"))
except (yaml.YAMLError, OSError, UnicodeError) as exc:
raise IntegrationValidationError(
f"Failed to read catalog config {config_path}: {exc}"
) from exc
if data is None:
data = {}
if not isinstance(data, dict):
raise IntegrationValidationError(
f"Catalog config file {config_path} is corrupted "
"(expected a mapping)."
)
catalogs = data.get("catalogs", [])
if not isinstance(catalogs, list):
raise IntegrationValidationError(
f"Catalog config {config_path} has invalid 'catalogs' value: "
"must be a list."
)
if not catalogs:
# An empty list is the kind of state that only happens if the
# user hand-edited the file; our own `remove_catalog` deletes
# the file when the last entry is popped. Surface a clear
# message instead of `out of range (0--1)`.
raise IntegrationValidationError(
"Catalog config contains no catalog entries."
)
# Map displayed index -> raw YAML index using the same priority
# defaulting as ``_load_catalog_config``. We deliberately stay
# tolerant here (no new validation errors) because the goal is
# only to mirror the order shown by ``catalog list``; entries
# that ``_load_catalog_config`` would have rejected outright
# would have failed ``catalog list`` already.
def _is_removable_catalog_entry(item: Any) -> bool:
if not isinstance(item, dict):
return False
raw_url = item.get("url")
if raw_url is None:
return False
return bool(str(raw_url).strip())
priority_pairs: List[Tuple[int, int]] = []
for yaml_idx, item in enumerate(catalogs):
if not _is_removable_catalog_entry(item):
continue
raw_priority = item.get("priority", yaml_idx + 1)
if isinstance(raw_priority, bool):
priority = yaml_idx + 1
else:
try:
priority = int(raw_priority)
except (TypeError, ValueError):
priority = yaml_idx + 1
priority_pairs.append((priority, yaml_idx))
if not priority_pairs:
raise IntegrationValidationError(
"Catalog config contains no removable catalog entries."
)
# Stable sort: ties keep their YAML order, matching list-view ordering.
priority_pairs.sort(key=lambda p: p[0])
display_order: List[int] = [yaml_idx for _, yaml_idx in priority_pairs]
if index < 0 or index >= len(display_order):
raise IntegrationValidationError(
f"Catalog index {index} out of range (0-{len(display_order) - 1})."
)
target_yaml_idx = display_order[index]
removed = catalogs.pop(target_yaml_idx)
if any(_is_removable_catalog_entry(item) for item in catalogs):
data["catalogs"] = catalogs
with open(config_path, "w", encoding="utf-8") as f:
yaml.dump(
data,
f,
default_flow_style=False,
sort_keys=False,
allow_unicode=True,
)
else:
# Removing the final entry: delete the config file rather than
# leaving behind an empty `catalogs:` list. `_load_catalog_config`
# treats an empty list as an error, so leaving the file would
# break every subsequent `integration` command until the user
# manually deletes `.specify/integration-catalogs.yml`.
# Deleting the file lets the project fall back to built-in
# defaults, which matches the behavior before any
# `catalog add` was ever run.
try:
config_path.unlink(missing_ok=True)
except OSError as exc:
raise IntegrationValidationError(
f"Failed to delete catalog config {config_path}: {exc}"
) from exc
fallback_name = f"catalog-{index + 1}"
if isinstance(removed, dict):
removed_name = removed.get("name")
if removed_name is not None:
normalized_name = str(removed_name).strip()
if normalized_name:
return normalized_name
removed_url = removed.get("url")
if removed_url is not None:
normalized_url = str(removed_url).strip()
if normalized_url:
return normalized_url
return fallback_name
# ---------------------------------------------------------------------------
# IntegrationDescriptor (integration.yml)

View File

@@ -53,6 +53,7 @@ class ClaudeIntegration(SkillsIntegration):
"extension": "/SKILL.md",
}
context_file = "CLAUDE.md"
multi_install_safe = True
@staticmethod
def inject_argument_hint(content: str, hint: str) -> str:

View File

@@ -19,3 +19,4 @@ class CodebuddyIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = "CODEBUDDY.md"
multi_install_safe = True

View File

@@ -27,6 +27,7 @@ class CodexIntegration(SkillsIntegration):
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"
multi_install_safe = True
def build_exec_args(
self,

View File

@@ -5,6 +5,10 @@ Copilot has several unique behaviors compared to standard markdown agents:
- Each command gets a companion ``.prompt.md`` file in ``.github/prompts/``
- Installs ``.vscode/settings.json`` with prompt file recommendations
- Context file lives at ``.github/copilot-instructions.md``
When ``--skills`` is passed via ``--integration-options``, Copilot scaffolds
commands as ``speckit-<name>/SKILL.md`` directories under ``.github/skills/``
instead. The two modes are mutually exclusive.
"""
from __future__ import annotations
@@ -16,7 +20,7 @@ import warnings
from pathlib import Path
from typing import Any
from ..base import IntegrationBase
from ..base import IntegrationBase, IntegrationOption, SkillsIntegration
from ..manifest import IntegrationManifest
@@ -44,12 +48,40 @@ def _allow_all() -> bool:
return True
class _CopilotSkillsHelper(SkillsIntegration):
"""Internal helper used when Copilot is scaffolded in skills mode.
Not registered in the integration registry — only used as a delegate
by ``CopilotIntegration`` when ``--skills`` is passed.
"""
key = "copilot"
config = {
"name": "GitHub Copilot",
"folder": ".github/",
"commands_subdir": "skills",
"install_url": "https://docs.github.com/en/copilot/concepts/agents/copilot-cli/about-copilot-cli",
"requires_cli": False,
}
registrar_config = {
"dir": ".github/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = ".github/copilot-instructions.md"
class CopilotIntegration(IntegrationBase):
"""Integration for GitHub Copilot (VS Code IDE + CLI).
The IDE integration (``requires_cli: False``) installs ``.agent.md``
command files. Workflow dispatch additionally requires the
``copilot`` CLI to be installed separately.
When ``--skills`` is passed via ``--integration-options``, commands
are scaffolded as ``speckit-<name>/SKILL.md`` under ``.github/skills/``
instead of the default ``.agent.md`` + ``.prompt.md`` layout.
"""
key = "copilot"
@@ -68,6 +100,30 @@ class CopilotIntegration(IntegrationBase):
}
context_file = ".github/copilot-instructions.md"
# Mutable flag set by setup() — indicates the active scaffolding mode.
_skills_mode: bool = False
def effective_invoke_separator(
self, parsed_options: dict[str, Any] | None = None
) -> str:
"""Return ``"-"`` when skills mode is requested, ``"."`` otherwise."""
if parsed_options and parsed_options.get("skills"):
return "-"
if self._skills_mode:
return "-"
return self.invoke_separator
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=False,
help="Scaffold commands as agent skills (speckit-<name>/SKILL.md) instead of .agent.md files",
),
]
def build_exec_args(
self,
prompt: str,
@@ -92,7 +148,19 @@ class CopilotIntegration(IntegrationBase):
return args
def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Copilot agents are not slash-commands — just return the args as prompt."""
"""Build the native invocation for a Copilot command.
Default mode: agents are not slash-commands — return args as prompt.
Skills mode: ``/speckit-<stem>`` slash-command dispatch.
"""
if self._skills_mode:
stem = command_name
if stem.startswith("speckit."):
stem = stem[len("speckit."):]
invocation = "/speckit-" + stem.replace(".", "-")
if args:
invocation = f"{invocation} {args}"
return invocation
return args or ""
def dispatch_command(
@@ -110,19 +178,37 @@ class CopilotIntegration(IntegrationBase):
Copilot ``.agent.md`` files are agents, not skills. The CLI
selects them with ``--agent <name>`` and the prompt is just
the user's arguments.
In skills mode, the prompt includes the skill invocation
(``/speckit-<stem>``).
"""
import subprocess
stem = command_name
if "." in stem:
stem = stem.rsplit(".", 1)[-1]
agent_name = f"speckit.{stem}"
if stem.startswith("speckit."):
stem = stem[len("speckit."):]
prompt = args or ""
cli_args = [
"copilot", "-p", prompt,
"--agent", agent_name,
]
# Detect skills mode from project layout when not set via setup()
skills_mode = self._skills_mode
if not skills_mode and project_root:
skills_dir = project_root / ".github" / "skills"
if skills_dir.is_dir():
skills_mode = any(
d.is_dir() and (d / "SKILL.md").is_file()
for d in skills_dir.glob("speckit-*")
)
if skills_mode:
prompt = "/speckit-" + stem.replace(".", "-")
if args:
prompt = f"{prompt} {args}"
else:
agent_name = f"speckit.{stem}"
prompt = args or ""
cli_args = ["copilot", "-p", prompt]
if not skills_mode:
cli_args.extend(["--agent", agent_name])
if _allow_all():
cli_args.append("--yolo")
if model:
@@ -168,6 +254,59 @@ class CopilotIntegration(IntegrationBase):
"""Copilot commands use ``.agent.md`` extension."""
return f"speckit.{template_name}.agent.md"
def post_process_skill_content(self, content: str) -> str:
"""Inject Copilot-specific ``mode:`` field into SKILL.md frontmatter.
Inserts ``mode: speckit.<stem>`` before the closing ``---`` so
Copilot can associate the skill with its agent mode.
"""
lines = content.splitlines(keepends=True)
# Extract skill name from frontmatter to derive the mode value
dash_count = 0
skill_name = ""
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1:
if stripped.startswith("mode:"):
return content # already present
if stripped.startswith("name:"):
# Parse: name: "speckit-plan" → speckit.plan
val = stripped.split(":", 1)[1].strip().strip('"').strip("'")
# Convert speckit-plan → speckit.plan
if val.startswith("speckit-"):
skill_name = "speckit." + val[len("speckit-"):]
else:
skill_name = val
if not skill_name:
return content
# Inject mode: before the closing --- of frontmatter
out: list[str] = []
dash_count = 0
injected = False
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2 and not injected:
if line.endswith("\r\n"):
eol = "\r\n"
elif line.endswith("\n"):
eol = "\n"
else:
eol = ""
out.append(f"mode: {skill_name}{eol}")
injected = True
out.append(line)
return "".join(out)
def setup(
self,
project_root: Path,
@@ -177,10 +316,24 @@ class CopilotIntegration(IntegrationBase):
) -> list[Path]:
"""Install copilot commands, companion prompts, and VS Code settings.
Uses base class primitives to: read templates, process them
(replace placeholders, strip script blocks, rewrite paths),
write as ``.agent.md``, then add companion prompts and VS Code settings.
When ``parsed_options["skills"]`` is truthy, delegates to skills
scaffolding (``speckit-<name>/SKILL.md`` under ``.github/skills/``).
Otherwise uses the default ``.agent.md`` + ``.prompt.md`` layout.
"""
parsed_options = parsed_options or {}
self._skills_mode = bool(parsed_options.get("skills"))
if self._skills_mode:
return self._setup_skills(project_root, manifest, parsed_options, **opts)
return self._setup_default(project_root, manifest, parsed_options, **opts)
def _setup_default(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Default mode: .agent.md + .prompt.md + VS Code settings merge."""
project_root_resolved = project_root.resolve()
if manifest.project_root != project_root_resolved:
raise ValueError(
@@ -252,6 +405,37 @@ class CopilotIntegration(IntegrationBase):
return created
def _setup_skills(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Skills mode: delegate to ``_CopilotSkillsHelper`` then post-process."""
helper = _CopilotSkillsHelper()
created = SkillsIntegration.setup(
helper, project_root, manifest, parsed_options, **opts
)
# Post-process generated skill files with Copilot-specific frontmatter
skills_dir = helper.skills_dest(project_root).resolve()
for path in created:
try:
path.resolve().relative_to(skills_dir)
except ValueError:
continue
if path.name != "SKILL.md":
continue
content = path.read_text(encoding="utf-8")
updated = self.post_process_skill_content(content)
if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)
return created
def _vscode_settings_path(self) -> Path | None:
"""Return path to the bundled vscode-settings.json template."""
tpl_dir = self.shared_templates_dir()

View File

@@ -26,6 +26,7 @@ class CursorAgentIntegration(SkillsIntegration):
}
context_file = ".cursor/rules/specify-rules.mdc"
multi_install_safe = True
@classmethod
def options(cls) -> list[IntegrationOption]:

View File

@@ -0,0 +1,65 @@
"""Devin for Terminal integration — skills-based agent.
Devin uses the ``.devin/skills/speckit-<name>/SKILL.md`` layout and
reads project context from ``AGENTS.md`` at the repo root. The CLI
binary is ``devin`` and skills are invoked via ``/<name>`` inside an
interactive ``devin`` session.
See: https://cli.devin.ai/docs/extensibility/skills/overview
"""
from __future__ import annotations
from ..base import IntegrationOption, SkillsIntegration
class DevinIntegration(SkillsIntegration):
"""Integration for Cognition AI's Devin for Terminal."""
key = "devin"
config = {
"name": "Devin for Terminal",
"folder": ".devin/",
"commands_subdir": "skills",
"install_url": "https://cli.devin.ai/docs",
"requires_cli": True,
}
registrar_config = {
"dir": ".devin/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": "/SKILL.md",
}
context_file = "AGENTS.md"
def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
) -> list[str] | None:
"""Build non-interactive CLI args for Devin for Terminal.
Devin supports ``devin -p <prompt>`` for single-turn execution
and ``--model`` for model selection, but its CLI has no flag
for structured JSON output. When ``output_json`` is requested,
Devin is still dispatched normally and returns plain-text
stdout instead of structured JSON. ``requires_cli=True`` is
kept on the integration for tool detection.
"""
args = [self.key, "-p", prompt]
if model:
args.extend(["--model", model])
return args
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills (default for Devin)",
),
]

View File

@@ -19,3 +19,4 @@ class GeminiIntegration(TomlIntegration):
"extension": ".toml",
}
context_file = "GEMINI.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class IflowIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = "IFLOW.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class JunieIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = ".junie/AGENTS.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class KilocodeIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = ".kilocode/rules/specify-rules.md"
multi_install_safe = True

View File

@@ -36,6 +36,7 @@ class KimiIntegration(SkillsIntegration):
"extension": "/SKILL.md",
}
context_file = "KIMI.md"
multi_install_safe = True
@classmethod
def options(cls) -> list[IntegrationOption]:

View File

@@ -11,6 +11,7 @@ from __future__ import annotations
import hashlib
import json
import os
import tempfile
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
@@ -47,6 +48,59 @@ def _validate_rel_path(rel: Path, root: Path) -> Path:
return resolved
def _manifest_path_label(root: Path, path: Path) -> str:
try:
return path.relative_to(root).as_posix()
except ValueError:
return path.as_posix()
def _ensure_safe_manifest_directory(root: Path, directory: Path) -> None:
"""Create a manifest directory without following symlinked parents."""
root_resolved = root.resolve()
try:
rel = directory.relative_to(root)
except ValueError:
label = _manifest_path_label(root, directory)
raise ValueError(f"Integration manifest directory escapes project root: {label}") from None
current = root
for part in rel.parts:
current = current / part
label = _manifest_path_label(root, current)
if current.is_symlink():
raise ValueError(f"Refusing to use symlinked integration manifest directory: {label}")
if current.exists():
if not current.is_dir():
raise ValueError(f"Integration manifest directory path is not a directory: {label}")
try:
current.resolve().relative_to(root_resolved)
except (OSError, ValueError):
raise ValueError(f"Integration manifest directory escapes project root: {label}") from None
continue
current.mkdir()
try:
current.resolve().relative_to(root_resolved)
except (OSError, ValueError):
raise ValueError(f"Integration manifest directory escapes project root: {label}") from None
def _ensure_safe_manifest_destination(root: Path, path: Path) -> None:
"""Refuse manifest writes that would escape the project or follow symlinks."""
root_resolved = root.resolve()
_ensure_safe_manifest_directory(root, path.parent)
label = _manifest_path_label(root, path)
if path.is_symlink():
raise ValueError(f"Refusing to overwrite symlinked integration manifest path: {label}")
if path.exists():
if not path.is_file():
raise ValueError(f"Integration manifest path is not a file: {label}")
try:
path.resolve().relative_to(root_resolved)
except (OSError, ValueError):
raise ValueError(f"Integration manifest path escapes project root: {label}") from None
class IntegrationManifest:
"""Tracks files installed by a single integration.
@@ -217,8 +271,19 @@ class IntegrationManifest:
"files": self._files,
}
path = self.manifest_path
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
content = json.dumps(data, indent=2) + "\n"
_ensure_safe_manifest_destination(self.project_root, path)
fd, temp_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
temp_path = Path(temp_name)
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
fh.write(content)
temp_path.chmod(0o644)
_ensure_safe_manifest_destination(self.project_root, path)
os.replace(temp_path, path)
finally:
if temp_path.exists():
temp_path.unlink()
return path
@classmethod

View File

@@ -19,3 +19,27 @@ class OpencodeIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = "AGENTS.md"
def build_exec_args(
self,
prompt: str,
*,
model: str | None = None,
output_json: bool = True,
) -> list[str] | None:
args = [self.key, "run"]
message = prompt
if prompt.startswith("/"):
command, _, remainder = prompt[1:].partition(" ")
if command:
args.extend(["--command", command])
message = remainder
if model:
args.extend(["-m", model])
if output_json:
args.extend(["--format", "json"])
if message:
args.append(message)
return args

View File

@@ -19,3 +19,4 @@ class QodercliIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = "QODER.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class QwenIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = "QWEN.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class RooIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = ".roo/rules/specify-rules.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class ShaiIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = "SHAI.md"
multi_install_safe = True

View File

@@ -19,3 +19,4 @@ class TabnineIntegration(TomlIntegration):
"extension": ".toml",
}
context_file = "TABNINE.md"
multi_install_safe = True

View File

@@ -27,6 +27,7 @@ class TraeIntegration(SkillsIntegration):
"extension": "/SKILL.md",
}
context_file = ".trae/rules/project_rules.md"
multi_install_safe = True
@classmethod
def options(cls) -> list[IntegrationOption]:

View File

@@ -1,21 +1,133 @@
"""Mistral Vibe CLI integration."""
"""
Mistral Vibe CLI integration — skills-based agent.
from ..base import MarkdownIntegration
Vibe uses ``.vibe/skills/speckit-<name>/SKILL.md`` layout (enforced since v2.0.0).
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from ..base import IntegrationOption, SkillsIntegration
from ..manifest import IntegrationManifest
class VibeIntegration(MarkdownIntegration):
class VibeIntegration(SkillsIntegration):
key = "vibe"
config = {
"name": "Mistral Vibe",
"folder": ".vibe/",
"commands_subdir": "prompts",
"commands_subdir": "skills",
"install_url": "https://github.com/mistralai/mistral-vibe",
"requires_cli": True,
}
registrar_config = {
"dir": ".vibe/prompts",
"dir": ".vibe/skills",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
"extension": "/SKILL.md",
}
context_file = ".vibe/agents/specify-agents.md"
context_file = "AGENTS.md"
@classmethod
def options(cls) -> list[IntegrationOption]:
return [
IntegrationOption(
"--skills",
is_flag=True,
default=True,
help="Install as agent skills",
),
]
@staticmethod
def _inject_frontmatter_flag(content: str, key: str, value: str = "true") -> str:
"""
Insert ``key: value`` before the closing ``---`` if not already present.
Value: true by default
"""
lines = content.splitlines(keepends=True)
# Pre-scan: bail out if already present in frontmatter
dash_count = 0
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2:
break
continue
if dash_count == 1 and stripped.startswith(f"{key}:"):
return content
# Inject before the closing --- of frontmatter
out: list[str] = []
dash_count = 0
injected = False
for line in lines:
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
if dash_count == 2 and not injected:
if line.endswith("\r\n"):
eol = "\r\n"
elif line.endswith("\n"):
eol = "\n"
else:
eol = ""
out.append(f"{key}: {value}{eol}")
injected = True
out.append(line)
return "".join(out)
def post_process_skill_content(self, content: str) -> str:
"""
Inject Vibe-specific frontmatter flags:
- user-invocable: allows the skill to be invoked by the user (not just other agents)
"""
updated = self._inject_frontmatter_flag(content, "user-invocable")
return updated
def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Vibe skills then inject Vibe-specific flags"""
import click
click.secho(
"Warning: The .vibe/skills layout requires Mistral Vibe v2.0.0 or newer. "
"Please ensure your installation is up to date.",
fg="yellow",
err=True,
)
created = super().setup(project_root, manifest, parsed_options=parsed_options, **opts)
# Post-process generated skill files
skills_dir = self.skills_dest(project_root).resolve()
for path in created:
# Only touch SKILL.md files under the skills directory
try:
path.resolve().relative_to(skills_dir)
except ValueError:
continue
if path.name != "SKILL.md":
continue
content_bytes = path.read_bytes()
content = content_bytes.decode("utf-8")
updated = self.post_process_skill_content(content)
if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)
return created

View File

@@ -19,3 +19,4 @@ class WindsurfIntegration(MarkdownIntegration):
"extension": ".md",
}
context_file = ".windsurf/rules/specify-rules.md"
multi_install_safe = True

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
"""Shared Spec Kit infrastructure installation helpers."""
from __future__ import annotations
import os
import tempfile
from pathlib import Path
from typing import Any
from .integrations.base import IntegrationBase
from .integrations.manifest import IntegrationManifest
def load_speckit_manifest(
project_path: Path,
*,
version: str,
console: Any | None = None,
) -> IntegrationManifest:
"""Load the shared infrastructure manifest, preserving existing entries."""
manifest_path = project_path / ".specify" / "integrations" / "speckit.manifest.json"
if manifest_path.exists():
try:
manifest = IntegrationManifest.load("speckit", project_path)
manifest.version = version
return manifest
except (ValueError, FileNotFoundError, OSError, UnicodeDecodeError) as exc:
if console is not None:
console.print(
f"[yellow]Warning:[/yellow] Could not read shared infrastructure "
f"manifest at {manifest_path}: {exc}"
)
console.print(
"A new shared manifest will be created; previously tracked "
"shared files may be treated as untracked."
)
return IntegrationManifest("speckit", project_path, version=version)
def shared_templates_source(
*,
core_pack: Path | None,
repo_root: Path,
) -> Path:
"""Return the bundled/source shared templates directory."""
if core_pack and (core_pack / "templates").is_dir():
return core_pack / "templates"
return repo_root / "templates"
def shared_scripts_source(
*,
core_pack: Path | None,
repo_root: Path,
) -> Path:
"""Return the bundled/source shared scripts directory."""
if core_pack and (core_pack / "scripts").is_dir():
return core_pack / "scripts"
return repo_root / "scripts"
def _shared_destination_label(project_path: Path, dest: Path) -> str:
try:
return dest.relative_to(project_path).as_posix()
except ValueError:
return str(dest)
def _shared_relative_path(project_path: Path, dest: Path) -> Path:
try:
rel = dest.relative_to(project_path)
except ValueError:
label = _shared_destination_label(project_path, dest)
raise ValueError(f"Shared infrastructure path escapes project root: {label}") from None
if rel.is_absolute() or ".." in rel.parts:
label = _shared_destination_label(project_path, dest)
raise ValueError(f"Shared infrastructure path escapes project root: {label}")
return rel
def _ensure_safe_shared_directory(project_path: Path, directory: Path, *, create: bool = True) -> None:
"""Create a shared infra directory without following symlinked parents."""
root = project_path.resolve()
rel = _shared_relative_path(project_path, directory)
current = project_path
for part in rel.parts:
current = current / part
label = _shared_destination_label(project_path, current)
if current.is_symlink():
raise ValueError(f"Refusing to use symlinked shared infrastructure directory: {label}")
if current.exists():
if not current.is_dir():
raise ValueError(f"Shared infrastructure directory path is not a directory: {label}")
try:
current.resolve().relative_to(root)
except (OSError, ValueError):
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
continue
if not create:
raise ValueError(f"Shared infrastructure directory does not exist: {label}")
current.mkdir()
if current.is_symlink():
raise ValueError(f"Refusing to use symlinked shared infrastructure directory: {label}")
try:
current.resolve().relative_to(root)
except (OSError, ValueError):
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
def _validate_safe_shared_directory(project_path: Path, directory: Path) -> None:
"""Validate existing directory parents while allowing missing directories."""
root = project_path.resolve()
rel = _shared_relative_path(project_path, directory)
current = project_path
for part in rel.parts:
current = current / part
label = _shared_destination_label(project_path, current)
if current.is_symlink():
raise ValueError(f"Refusing to use symlinked shared infrastructure directory: {label}")
if not current.exists():
continue
if not current.is_dir():
raise ValueError(f"Shared infrastructure directory path is not a directory: {label}")
try:
current.resolve().relative_to(root)
except (OSError, ValueError):
raise ValueError(f"Shared infrastructure directory escapes project root: {label}") from None
def _ensure_safe_shared_destination(
project_path: Path,
dest: Path,
*,
parent_must_exist: bool = True,
) -> None:
"""Refuse shared infra writes that would escape or follow symlinks."""
root = project_path.resolve()
_shared_relative_path(project_path, dest)
if parent_must_exist:
_ensure_safe_shared_directory(project_path, dest.parent, create=False)
else:
_validate_safe_shared_directory(project_path, dest.parent)
label = _shared_destination_label(project_path, dest)
if dest.is_symlink():
raise ValueError(f"Refusing to overwrite symlinked shared infrastructure path: {label}")
if dest.exists():
try:
dest.resolve().relative_to(root)
except (OSError, ValueError):
raise ValueError(f"Shared infrastructure destination escapes project root: {label}") from None
def _write_shared_text(project_path: Path, dest: Path, content: str) -> None:
_write_shared_bytes(project_path, dest, content.encode("utf-8"))
def _write_shared_bytes(
project_path: Path,
dest: Path,
content: bytes,
*,
mode: int = 0o644,
) -> None:
_ensure_safe_shared_destination(project_path, dest)
fd, temp_name = tempfile.mkstemp(prefix=f".{dest.name}.", dir=dest.parent)
temp_path = Path(temp_name)
try:
with os.fdopen(fd, "wb") as fh:
fh.write(content)
temp_path.chmod(mode)
_ensure_safe_shared_destination(project_path, dest)
os.replace(temp_path, dest)
finally:
if temp_path.exists():
temp_path.unlink()
def refresh_shared_templates(
project_path: Path,
*,
version: str,
core_pack: Path | None,
repo_root: Path,
console: Any,
invoke_separator: str,
force: bool = False,
) -> None:
"""Refresh default-sensitive shared templates without touching scripts."""
templates_src = shared_templates_source(core_pack=core_pack, repo_root=repo_root)
if not templates_src.is_dir():
return
manifest = load_speckit_manifest(project_path, version=version, console=console)
tracked_files = manifest.files
modified = set(manifest.check_modified())
skipped_files: list[str] = []
planned_updates: list[tuple[Path, str, str]] = []
dest_templates = project_path / ".specify" / "templates"
_ensure_safe_shared_directory(project_path, dest_templates)
for src in templates_src.iterdir():
if not src.is_file() or src.name == "vscode-settings.json" or src.name.startswith("."):
continue
dst = dest_templates / src.name
_ensure_safe_shared_destination(project_path, dst)
rel = dst.relative_to(project_path).as_posix()
if dst.exists() and not force:
if rel not in tracked_files or rel in modified:
skipped_files.append(rel)
continue
content = src.read_text(encoding="utf-8")
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
planned_updates.append((dst, rel, content))
for dst, rel, content in planned_updates:
_write_shared_text(project_path, dst, content)
manifest.record_existing(rel)
manifest.save()
if skipped_files:
console.print(
f"[yellow]⚠[/yellow] {len(skipped_files)} modified or untracked shared template file(s) were not updated:"
)
for rel in skipped_files:
console.print(f" {rel}")
def install_shared_infra(
project_path: Path,
script_type: str,
*,
version: str,
core_pack: Path | None,
repo_root: Path,
console: Any,
force: bool = False,
invoke_separator: str = ".",
) -> bool:
"""Install shared scripts and templates into *project_path*."""
manifest = load_speckit_manifest(project_path, version=version, console=console)
skipped_files: list[str] = []
planned_copies: list[tuple[Path, str, bytes, int]] = []
planned_templates: list[tuple[Path, str, str]] = []
scripts_src = shared_scripts_source(core_pack=core_pack, repo_root=repo_root)
if scripts_src.is_dir():
dest_scripts = project_path / ".specify" / "scripts"
_ensure_safe_shared_directory(project_path, dest_scripts)
variant_dir = "bash" if script_type == "sh" else "powershell"
variant_src = scripts_src / variant_dir
if variant_src.is_dir():
dest_variant = dest_scripts / variant_dir
_ensure_safe_shared_directory(project_path, dest_variant)
for src_path in variant_src.rglob("*"):
if not src_path.is_file():
continue
rel_path = src_path.relative_to(variant_src)
dst_path = dest_variant / rel_path
_ensure_safe_shared_destination(project_path, dst_path, parent_must_exist=False)
if dst_path.exists() and not force:
skipped_files.append(dst_path.relative_to(project_path).as_posix())
continue
_ensure_safe_shared_directory(project_path, dst_path.parent)
rel = dst_path.relative_to(project_path).as_posix()
planned_copies.append((dst_path, rel, src_path.read_bytes(), src_path.stat().st_mode & 0o777))
templates_src = shared_templates_source(core_pack=core_pack, repo_root=repo_root)
if templates_src.is_dir():
dest_templates = project_path / ".specify" / "templates"
_ensure_safe_shared_directory(project_path, dest_templates)
for src in templates_src.iterdir():
if not src.is_file() or src.name == "vscode-settings.json" or src.name.startswith("."):
continue
dst = dest_templates / src.name
_ensure_safe_shared_destination(project_path, dst)
if dst.exists() and not force:
skipped_files.append(dst.relative_to(project_path).as_posix())
continue
content = src.read_text(encoding="utf-8")
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
rel = dst.relative_to(project_path).as_posix()
planned_templates.append((dst, rel, content))
for dst_path, rel, content, mode in planned_copies:
_ensure_safe_shared_directory(project_path, dst_path.parent)
_write_shared_bytes(project_path, dst_path, content, mode=mode)
manifest.record_existing(rel)
for dst, rel, content in planned_templates:
_write_shared_text(project_path, dst, content)
manifest.record_existing(rel)
if skipped_files:
console.print(
f"[yellow]⚠[/yellow] {len(skipped_files)} shared infrastructure file(s) already exist and were not updated:"
)
for path in skipped_files:
console.print(f" {path}")
console.print(
"To refresh shared infrastructure, run "
"[cyan]specify init --here --force[/cyan] or "
"[cyan]specify integration upgrade --force[/cyan]."
)
manifest.save()
return True

View File

@@ -4,13 +4,13 @@
**Created**: [DATE]
**Feature**: [Link to spec.md or relevant documentation]
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
**Note**: This checklist is generated by the `__SPECKIT_COMMAND_CHECKLIST__` command based on feature context and requirements.
<!--
============================================================================
IMPORTANT: The checklist items below are SAMPLE ITEMS for illustration only.
The /speckit.checklist command MUST replace these with actual items based on:
The __SPECKIT_COMMAND_CHECKLIST__ command MUST replace these with actual items based on:
- User's specific checklist request
- Feature requirements from spec.md
- Technical context from plan.md

View File

@@ -49,13 +49,13 @@ You **MUST** consider the user input before proceeding (if not empty).
## Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `__SPECKIT_COMMAND_TASKS__` has successfully produced a complete `tasks.md`.
## Operating Constraints
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
**Constitution Authority**: The project constitution (`/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
**Constitution Authority**: The project constitution (`/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `__SPECKIT_COMMAND_ANALYZE__`.
## Execution Steps
@@ -191,9 +191,9 @@ Output a Markdown report (no file writes) with the following structure:
At end of report, output a concise Next Actions block:
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
- If CRITICAL issues exist: Recommend resolving before `__SPECKIT_COMMAND_IMPLEMENT__`
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
- Provide explicit command suggestions: e.g., "Run __SPECKIT_COMMAND_SPECIFY__ with refinement", "Run __SPECKIT_COMMAND_PLAN__ to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
### 8. Offer Remediation

View File

@@ -249,7 +249,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- Actor/timing
- Any explicit user-specified must-have items incorporated
**Important**: Each `/speckit.checklist` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
**Important**: Each `__SPECKIT_COMMAND_CHECKLIST__` command invocation uses a short, descriptive checklist filename and either creates a new file or appends to an existing one. This allows:
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
- Simple, memorable filenames that indicate checklist purpose

View File

@@ -55,7 +55,7 @@ You **MUST** consider the user input before proceeding (if not empty).
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `__SPECKIT_COMMAND_PLAN__`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
Execution steps:
@@ -63,7 +63,7 @@ Execution steps:
- `FEATURE_DIR`
- `FEATURE_SPEC`
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
- 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. 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).
@@ -202,13 +202,13 @@ Execution steps:
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
- If any Outstanding or Deferred remain, recommend whether to proceed to `__SPECKIT_COMMAND_PLAN__` or run `__SPECKIT_COMMAND_CLARIFY__` again later post-plan.
- Suggested next command.
Behavior rules:
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
- If spec file missing, instruct user to run `__SPECKIT_COMMAND_SPECIFY__` first (do not create a new spec here).
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
- Respect user early termination signals ("stop", "done", "proceed").

View File

@@ -88,6 +88,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- **IF EXISTS**: Read data-model.md for entities and relationships
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
- **IF EXISTS**: Read research.md for technical decisions and constraints
- **IF EXISTS**: Read /memory/constitution.md for governance constraints
- **IF EXISTS**: Read quickstart.md for integration scenarios
4. **Project Setup Verification**:
@@ -169,7 +170,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- Confirm the implementation follows the technical plan
- Report final status with summary of completed work
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `__SPECKIT_COMMAND_TASKS__` first to regenerate the task list.
10. **Check for extension hooks**: After completion validation, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_implement` key

View File

@@ -54,7 +54,7 @@ You **MUST** consider the user input before proceeding (if not empty).
## Outline
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
The text the user typed after `__SPECKIT_COMMAND_SPECIFY__` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `{ARGS}` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
Given that feature description, do this:
@@ -100,10 +100,10 @@ Given that feature description, do this:
}
```
Write the actual resolved directory path value (for example, `specs/003-user-auth`), not the literal string `SPECIFY_FEATURE_DIRECTORY`.
This allows downstream commands (`/speckit.plan`, `/speckit.tasks`, etc.) to locate the feature directory without relying on git branch name conventions.
This allows downstream commands (`__SPECKIT_COMMAND_PLAN__`, `__SPECKIT_COMMAND_TASKS__`, etc.) to locate the feature directory without relying on git branch name conventions.
**IMPORTANT**:
- You must only create one feature per `/speckit.specify` invocation
- You must only create one feature per `__SPECKIT_COMMAND_SPECIFY__` invocation
- The spec directory name and the git branch name are independent — they may be the same but that is the user's choice
- The spec directory and file are always created by this command, never by the hook
@@ -174,7 +174,7 @@ Given that feature description, do this:
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
- Items marked incomplete require spec updates before `__SPECKIT_COMMAND_CLARIFY__` or `__SPECKIT_COMMAND_PLAN__`
```
b. **Run Validation Check**: Review the spec against each checklist item:
@@ -183,7 +183,7 @@ Given that feature description, do this:
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to step 7
- **If all items pass**: Mark checklist complete and proceed to step 8
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
@@ -232,7 +232,7 @@ Given that feature description, do this:
- `SPECIFY_FEATURE_DIRECTORY` — the feature directory path
- `SPEC_FILE` — the spec file path
- Checklist results summary
- Readiness for the next phase (`/speckit.clarify` or `/speckit.plan`)
- Readiness for the next phase (`__SPECKIT_COMMAND_CLARIFY__` or `__SPECKIT_COMMAND_PLAN__`)
9. **Check for extension hooks**: After reporting completion, check if `.specify/extensions.yml` exists in the project root.
- If it exists, read it and look for entries under the `hooks.after_specify` key

View File

@@ -10,8 +10,8 @@ handoffs:
prompt: Start the implementation in phases
send: true
scripts:
sh: scripts/bash/check-prerequisites.sh --json
ps: scripts/powershell/check-prerequisites.ps1 -Json
sh: scripts/bash/setup-tasks.sh --json
ps: scripts/powershell/setup-tasks.ps1 -Json
---
## User Input
@@ -58,7 +58,7 @@ You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: 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. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE, and AVAILABLE_DOCS list. `FEATURE_DIR` and `TASKS_TEMPLATE` must be absolute paths when provided. `AVAILABLE_DOCS` is a list of document names/relative paths available under `FEATURE_DIR` (for example `research.md` or `contracts/`). 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. **Load design documents**: Read from FEATURE_DIR:
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
@@ -76,7 +76,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- Create parallel execution examples per user story
- Validate task completeness (each user story has all needed tasks, independently testable)
4. **Generate tasks.md**: Use `templates/tasks-template.md` as structure, fill with:
4. **Generate tasks.md**: Read the tasks template from TASKS_TEMPLATE (from the JSON output above) and use it as structure. If TASKS_TEMPLATE is empty, fall back to `.specify/templates/tasks-template.md`. Fill with:
- Correct feature name from plan.md
- Phase 1: Setup tasks (project initialization)
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)

View File

@@ -3,7 +3,7 @@
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/templates/plan-template.md` for the execution workflow.
**Note**: This template is filled in by the `__SPECKIT_COMMAND_PLAN__` command. See `.specify/templates/plan-template.md` for the execution workflow.
## Summary
@@ -39,12 +39,12 @@
```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
├── plan.md # This file (__SPECKIT_COMMAND_PLAN__ command output)
├── research.md # Phase 0 output (__SPECKIT_COMMAND_PLAN__ command)
├── data-model.md # Phase 1 output (__SPECKIT_COMMAND_PLAN__ command)
├── quickstart.md # Phase 1 output (__SPECKIT_COMMAND_PLAN__ command)
├── contracts/ # Phase 1 output (__SPECKIT_COMMAND_PLAN__ command)
└── tasks.md # Phase 2 output (__SPECKIT_COMMAND_TASKS__ command - NOT created by __SPECKIT_COMMAND_PLAN__)
```
### Source Code (repository root)

View File

@@ -29,7 +29,7 @@ description: "Task list template for feature implementation"
============================================================================
IMPORTANT: The tasks below are SAMPLE TASKS for illustration purposes only.
The /speckit.tasks command MUST replace these with actual tasks based on:
The __SPECKIT_COMMAND_TASKS__ command MUST replace these with actual tasks based on:
- User stories from spec.md (with their priorities P1, P2, P3...)
- Feature requirements from plan.md
- Entities from data-model.md

View File

@@ -6,6 +6,7 @@ from specify_cli.integrations.base import (
IntegrationBase,
IntegrationOption,
MarkdownIntegration,
SkillsIntegration,
)
from specify_cli.integrations.manifest import IntegrationManifest
from .conftest import StubIntegration
@@ -167,3 +168,130 @@ class TestBasePrimitives:
assert f.parent.name == "commands"
assert f.name.startswith("speckit.")
assert f.name.endswith(".md")
class TestBuildCommandInvocation:
"""Tests for build_command_invocation across integration types."""
def test_base_core_command_dotted(self):
i = StubIntegration()
assert i.build_command_invocation("speckit.plan") == "/speckit.plan"
def test_base_core_command_bare(self):
i = StubIntegration()
assert i.build_command_invocation("plan") == "/speckit.plan"
def test_base_core_command_with_args(self):
i = StubIntegration()
assert i.build_command_invocation("plan", "my feature") == "/speckit.plan my feature"
def test_base_extension_command(self):
i = StubIntegration()
assert i.build_command_invocation("speckit.git.commit") == "/speckit.git.commit"
def test_base_extension_command_bare(self):
i = StubIntegration()
assert i.build_command_invocation("git.commit") == "/speckit.git.commit"
def test_skills_core_command(self):
from specify_cli.integrations import get_integration
i = get_integration("codex")
assert i.build_command_invocation("speckit.plan") == "/speckit-plan"
assert i.build_command_invocation("plan") == "/speckit-plan"
def test_skills_extension_command(self):
from specify_cli.integrations import get_integration
i = get_integration("codex")
assert i.build_command_invocation("speckit.git.commit") == "/speckit-git-commit"
assert i.build_command_invocation("git.commit") == "/speckit-git-commit"
def test_skills_extension_command_with_args(self):
from specify_cli.integrations import get_integration
i = get_integration("codex")
assert i.build_command_invocation("speckit.git.commit", "fix typo") == "/speckit-git-commit fix typo"
class TestResolveCommandRefs:
"""Tests for __SPECKIT_COMMAND_<NAME>__ placeholder resolution."""
def test_dot_separator_core_command(self):
text = "Run `__SPECKIT_COMMAND_PLAN__` to plan."
result = IntegrationBase.resolve_command_refs(text, ".")
assert result == "Run `/speckit.plan` to plan."
def test_hyphen_separator_core_command(self):
text = "Run `__SPECKIT_COMMAND_PLAN__` to plan."
result = IntegrationBase.resolve_command_refs(text, "-")
assert result == "Run `/speckit-plan` to plan."
def test_multiple_placeholders(self):
text = "__SPECKIT_COMMAND_SPECIFY__ then __SPECKIT_COMMAND_PLAN__ then __SPECKIT_COMMAND_TASKS__"
result = IntegrationBase.resolve_command_refs(text, ".")
assert result == "/speckit.specify then /speckit.plan then /speckit.tasks"
def test_extension_command_dot(self):
text = "Run __SPECKIT_COMMAND_GIT_COMMIT__ to commit."
result = IntegrationBase.resolve_command_refs(text, ".")
assert result == "Run /speckit.git.commit to commit."
def test_extension_command_hyphen(self):
text = "Run __SPECKIT_COMMAND_GIT_COMMIT__ to commit."
result = IntegrationBase.resolve_command_refs(text, "-")
assert result == "Run /speckit-git-commit to commit."
def test_no_placeholders_unchanged(self):
text = "No placeholders here."
assert IntegrationBase.resolve_command_refs(text, ".") == text
def test_default_separator_is_dot(self):
text = "__SPECKIT_COMMAND_PLAN__"
assert IntegrationBase.resolve_command_refs(text) == "/speckit.plan"
def test_invoke_separator_class_attribute(self):
assert IntegrationBase.invoke_separator == "."
assert SkillsIntegration.invoke_separator == "-"
def test_effective_invoke_separator_default(self):
"""Base classes return invoke_separator regardless of parsed_options."""
from .conftest import StubIntegration
stub = StubIntegration()
assert stub.effective_invoke_separator() == "."
assert stub.effective_invoke_separator({"skills": True}) == "."
def test_process_template_resolves_placeholders(self):
content = "---\ndescription: test\n---\nRun __SPECKIT_COMMAND_PLAN__ now."
result = IntegrationBase.process_template(
content, "test-agent", "sh", invoke_separator="."
)
assert "/speckit.plan" in result
assert "__SPECKIT_COMMAND_" not in result
def test_process_template_skills_separator(self):
content = "---\ndescription: test\n---\nRun __SPECKIT_COMMAND_PLAN__ now."
result = IntegrationBase.process_template(
content, "test-agent", "sh", invoke_separator="-"
)
assert "/speckit-plan" in result
assert "__SPECKIT_COMMAND_" not in result
def test_unclosed_placeholder_unchanged(self):
text = "Run __SPECKIT_COMMAND_PLAN to plan."
assert IntegrationBase.resolve_command_refs(text, ".") == text
def test_empty_name_not_matched(self):
text = "Run __SPECKIT_COMMAND___ to plan."
assert IntegrationBase.resolve_command_refs(text, ".") == text
def test_lowercase_placeholder_not_matched(self):
text = "Run __SPECKIT_COMMAND_plan__ to plan."
assert IntegrationBase.resolve_command_refs(text, ".") == text
def test_placeholder_adjacent_to_text(self):
text = "foo__SPECKIT_COMMAND_PLAN__bar"
result = IntegrationBase.resolve_command_refs(text, ".")
assert result == "foo/speckit.planbar"
def test_placeholder_with_digits(self):
text = "__SPECKIT_COMMAND_V2_PLAN__"
result = IntegrationBase.resolve_command_refs(text, ".")
assert result == "/speckit.v2.plan"

File diff suppressed because it is too large Load Diff

View File

@@ -98,6 +98,7 @@ class MarkdownIntegrationTests:
assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}"
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
assert "__SPECKIT_COMMAND_" not in content, f"{f.name} has unprocessed __SPECKIT_COMMAND_*__"
assert "\nscripts:\n" not in content, f"{f.name} has unstripped scripts: block"
def test_plan_references_correct_context_file(self, tmp_path):
@@ -273,11 +274,11 @@ class MarkdownIntegrationTests:
if script_variant == "sh":
for name in ["check-prerequisites.sh", "common.sh", "create-new-feature.sh",
"setup-plan.sh"]:
"setup-plan.sh", "setup-tasks.sh"]:
files.append(f".specify/scripts/bash/{name}")
else:
for name in ["check-prerequisites.ps1", "common.ps1", "create-new-feature.ps1",
"setup-plan.ps1"]:
"setup-plan.ps1", "setup-tasks.ps1"]:
files.append(f".specify/scripts/powershell/{name}")
for name in ["checklist-template.md",

View File

@@ -159,6 +159,22 @@ class SkillsIntegrationTests:
assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}"
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
assert "__SPECKIT_COMMAND_" not in content, f"{f.name} has unprocessed __SPECKIT_COMMAND_*__"
def test_command_refs_use_hyphen_separator(self, tmp_path):
"""Skills agents must resolve command refs with hyphen separator."""
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
created = i.setup(tmp_path, m)
skill_files = [f for f in created if "scripts" not in f.parts]
assert len(skill_files) > 0
for f in skill_files:
content = f.read_text(encoding="utf-8")
# Skills agents must use /speckit-<name>, not /speckit.<name>
assert "/speckit." not in content, (
f"{f.name} contains dot-notation /speckit. reference; "
f"skills agents must use /speckit-<name>"
)
def test_skill_body_has_content(self, tmp_path):
"""Each SKILL.md body should contain template content after the frontmatter."""
@@ -371,6 +387,7 @@ class SkillsIntegrationTests:
".specify/scripts/bash/common.sh",
".specify/scripts/bash/create-new-feature.sh",
".specify/scripts/bash/setup-plan.sh",
".specify/scripts/bash/setup-tasks.sh",
]
else:
files += [
@@ -378,6 +395,7 @@ class SkillsIntegrationTests:
".specify/scripts/powershell/common.ps1",
".specify/scripts/powershell/create-new-feature.ps1",
".specify/scripts/powershell/setup-plan.ps1",
".specify/scripts/powershell/setup-tasks.ps1",
]
# Templates
files += [

View File

@@ -106,6 +106,7 @@ class TomlIntegrationTests:
assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}"
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
assert "__SPECKIT_COMMAND_" not in content, f"{f.name} has unprocessed __SPECKIT_COMMAND_*__"
def test_toml_has_description(self, tmp_path):
"""Every TOML command file should have a description key."""
@@ -515,6 +516,7 @@ class TomlIntegrationTests:
"common.sh",
"create-new-feature.sh",
"setup-plan.sh",
"setup-tasks.sh",
]:
files.append(f".specify/scripts/bash/{name}")
else:
@@ -523,6 +525,7 @@ class TomlIntegrationTests:
"common.ps1",
"create-new-feature.ps1",
"setup-plan.ps1",
"setup-tasks.ps1",
]:
files.append(f".specify/scripts/powershell/{name}")

View File

@@ -105,6 +105,7 @@ class YamlIntegrationTests:
assert "{SCRIPT}" not in content, f"{f.name} has unprocessed {{SCRIPT}}"
assert "__AGENT__" not in content, f"{f.name} has unprocessed __AGENT__"
assert "{ARGS}" not in content, f"{f.name} has unprocessed {{ARGS}}"
assert "__SPECKIT_COMMAND_" not in content, f"{f.name} has unprocessed __SPECKIT_COMMAND_*__"
def test_yaml_has_title(self, tmp_path):
"""Every YAML recipe should have a title field."""
@@ -394,6 +395,7 @@ class YamlIntegrationTests:
"common.sh",
"create-new-feature.sh",
"setup-plan.sh",
"setup-tasks.sh",
]:
files.append(f".specify/scripts/bash/{name}")
else:
@@ -402,6 +404,7 @@ class YamlIntegrationTests:
"common.ps1",
"create-new-feature.ps1",
"setup-plan.ps1",
"setup-tasks.ps1",
]:
files.append(f".specify/scripts/powershell/{name}")

View File

@@ -12,6 +12,7 @@ from specify_cli.integrations.catalog import (
IntegrationCatalogError,
IntegrationDescriptor,
IntegrationDescriptorError,
IntegrationValidationError,
)
@@ -115,8 +116,45 @@ class TestActiveCatalogs:
cfg = specify / "integration-catalogs.yml"
cfg.write_text(yaml.dump({"catalogs": []}))
cat = IntegrationCatalog(tmp_path)
with pytest.raises(IntegrationCatalogError, match="no 'catalogs' entries"):
with pytest.raises(IntegrationCatalogError, match="no 'catalogs' entries") as exc_info:
cat.get_active_catalogs()
assert str(cfg) in str(exc_info.value)
def test_empty_config_file_raises_no_catalogs(self, tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.delenv("SPECKIT_INTEGRATION_CATALOG_URL", raising=False)
specify = tmp_path / ".specify"
specify.mkdir()
cfg = specify / "integration-catalogs.yml"
cfg.write_text("", encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="no 'catalogs' entries"
) as exc_info:
cat.get_active_catalogs()
assert str(cfg) in str(exc_info.value)
@pytest.mark.parametrize("config_content", ["[]\n", "false\n", "0\n", "''\n"])
def test_load_catalog_config_rejects_falsy_non_mapping_roots(
self, tmp_path, monkeypatch, config_content
):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.delenv("SPECKIT_INTEGRATION_CATALOG_URL", raising=False)
specify = tmp_path / ".specify"
specify.mkdir()
cfg = specify / "integration-catalogs.yml"
cfg.write_text(config_content, encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError,
match="expected a YAML mapping at the root",
) as exc_info:
cat.get_active_catalogs()
assert str(cfg) in str(exc_info.value)
# ---------------------------------------------------------------------------
@@ -632,7 +670,7 @@ class TestIntegrationUpgrade:
finally:
os.chdir(old)
assert result.exit_code != 0
assert "not the currently installed integration" in result.output
assert "not installed" in result.output
def test_upgrade_no_manifest(self, tmp_path):
"""Upgrade with missing manifest suggests fresh install."""
@@ -654,3 +692,838 @@ class TestIntegrationUpgrade:
os.chdir(old)
assert result.exit_code == 0
assert "Nothing to upgrade" in result.output
# ---------------------------------------------------------------------------
# IntegrationCatalog — catalog source management (get_catalog_configs / add / remove)
# ---------------------------------------------------------------------------
class TestCatalogSourceManagement:
"""Unit tests for add_catalog / remove_catalog / get_catalog_configs."""
def _isolate(self, tmp_path, monkeypatch):
"""Point HOME at tmp_path and clear the env override so we read built-ins."""
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("USERPROFILE", str(tmp_path))
monkeypatch.delenv("SPECKIT_INTEGRATION_CATALOG_URL", raising=False)
(tmp_path / ".specify").mkdir()
def test_get_catalog_configs_returns_builtin_stack(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
configs = cat.get_catalog_configs()
assert [c["name"] for c in configs] == ["default", "community"]
assert all(isinstance(c["url"], str) and c["url"] for c in configs)
assert configs[0]["install_allowed"] is True
assert configs[1]["install_allowed"] is False
def test_add_catalog_creates_config_file(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://new.example.com/catalog.json", name="mine")
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
assert cfg_path.exists()
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert data["catalogs"] == [
{
"name": "mine",
"url": "https://new.example.com/catalog.json",
"priority": 1,
"install_allowed": True,
"description": "",
}
]
# Round-trip: active catalogs should now come from the config file.
active = cat.get_active_catalogs()
assert [e.name for e in active] == ["mine"]
def test_add_catalog_recovers_from_empty_config_file(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text("", encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://example.com/catalog.json")
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert data["catalogs"] == [
{
"name": "catalog-1",
"url": "https://example.com/catalog.json",
"priority": 1,
"install_allowed": True,
"description": "",
}
]
@pytest.mark.parametrize("config_content", ["[]\n", "false\n", "0\n", "''\n"])
def test_add_catalog_rejects_falsy_non_mapping_config_roots(
self, tmp_path, monkeypatch, config_content
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(config_content, encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError,
match="corrupted.*expected a mapping",
) as exc_info:
cat.add_catalog("https://example.com/catalog.json")
assert str(cfg_path) in str(exc_info.value)
def test_add_catalog_auto_derives_name_and_priority(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://a.example.com/catalog.json")
cat.add_catalog("https://b.example.com/catalog.json")
data = yaml.safe_load(
(tmp_path / ".specify" / "integration-catalogs.yml").read_text(encoding="utf-8")
)
entries = data["catalogs"]
assert [e["name"] for e in entries] == ["catalog-1", "catalog-2"]
assert [e["priority"] for e in entries] == [1, 2]
def test_add_catalog_normalizes_name(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://a.example.com/catalog.json", name=" mine ")
cat.add_catalog("https://b.example.com/catalog.json", name=" ")
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
entries = data["catalogs"]
assert [e["name"] for e in entries] == ["mine", "catalog-2"]
def test_add_catalog_rejects_duplicate_url(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://dup.example.com/catalog.json")
with pytest.raises(IntegrationValidationError, match="already configured"):
cat.add_catalog("https://dup.example.com/catalog.json")
def test_add_catalog_rejects_invalid_url(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(IntegrationCatalogError, match="HTTPS"):
cat.add_catalog("http://insecure.example.com/catalog.json")
assert not (tmp_path / ".specify" / "integration-catalogs.yml").exists()
def test_add_catalog_rejects_empty_url(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(IntegrationValidationError, match="must be non-empty"):
cat.add_catalog(" ")
assert not (tmp_path / ".specify" / "integration-catalogs.yml").exists()
def test_remove_catalog_without_config_errors(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(IntegrationValidationError, match="No catalog config"):
cat.remove_catalog(0)
def test_remove_catalog_happy_path(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://a.example.com/catalog.json", name="a")
cat.add_catalog("https://b.example.com/catalog.json", name="b")
removed = cat.remove_catalog(0)
assert removed == "a"
data = yaml.safe_load(
(tmp_path / ".specify" / "integration-catalogs.yml").read_text(encoding="utf-8")
)
assert [e["name"] for e in data["catalogs"]] == ["b"]
def test_remove_catalog_index_out_of_range(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://a.example.com/catalog.json", name="a")
with pytest.raises(IntegrationValidationError, match="out of range"):
cat.remove_catalog(5)
with pytest.raises(IntegrationValidationError, match="out of range"):
cat.remove_catalog(-1)
def test_corrupt_config_rejected_on_add(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text("- just\n- a\n- list\n", encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(IntegrationValidationError, match="corrupted") as exc_info:
cat.add_catalog("https://new.example.com/catalog.json")
assert str(cfg_path) in str(exc_info.value)
def test_add_catalog_rejects_non_list_catalogs_with_config_path(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump({"catalogs": "not-a-list"}), encoding="utf-8"
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="invalid 'catalogs' value"
) as exc_info:
cat.add_catalog("https://new.example.com/catalog.json")
assert str(cfg_path) in str(exc_info.value)
def test_add_catalog_rejects_non_mapping_entry_with_config_path(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump({"catalogs": ["not-a-mapping"]}), encoding="utf-8"
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="Invalid catalog entry at index 0"
) as exc_info:
cat.add_catalog("https://new.example.com/catalog.json")
message = str(exc_info.value)
assert str(cfg_path) in message
assert "expected a mapping" in message
def test_add_catalog_skips_blank_url_entries(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": " ", "name": "blank", "priority": 99},
{
"url": "https://a.example.com/catalog.json",
"name": "a",
"priority": 5,
},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://b.example.com/catalog.json", name="b")
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert data["catalogs"][-1]["name"] == "b"
assert data["catalogs"][-1]["priority"] == 6
def test_add_catalog_default_name_ignores_blank_url_entries(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump({"catalogs": [{"url": " ", "name": "blank"}]}),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://example.com/catalog.json")
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert data["catalogs"][-1]["name"] == "catalog-1"
def test_add_catalog_rejects_non_integer_priority(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{
"url": "https://a.example.com/catalog.json",
"name": "a",
"priority": "first",
}
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError,
match="'priority' must be an integer, got 'first'",
):
cat.add_catalog("https://b.example.com/catalog.json")
def test_add_catalog_accepts_numeric_string_priority(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{
"url": "https://a.example.com/catalog.json",
"name": "a",
"priority": "10",
}
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://b.example.com/catalog.json", name="b")
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert data["catalogs"][-1]["name"] == "b"
assert data["catalogs"][-1]["priority"] == 11
@pytest.mark.parametrize(
("bad_url", "reason"),
[
("http://insecure.example.com/catalog.json", "HTTPS"),
(123, "HTTPS"),
],
)
def test_add_catalog_rejects_existing_entry_with_bad_url(
self, tmp_path, monkeypatch, bad_url, reason
):
"""A sibling entry with an http:// URL should block a new add."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{
"url": bad_url,
"name": "bad",
}
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(IntegrationValidationError) as exc_info:
cat.add_catalog("https://good.example.com/catalog.json")
message = str(exc_info.value)
assert str(cfg_path) in message
assert "index 0" in message
assert reason in message
def test_add_catalog_wraps_yaml_parse_errors(self, tmp_path, monkeypatch):
"""Invalid YAML on disk surfaces as IntegrationValidationError, not a raw YAMLError."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
invalid_yaml = "catalogs:\n - url: 'https://a.example.com/cat.json'\n - [bad\n"
cfg_path.write_text(invalid_yaml, encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="Failed to read catalog config"
):
cat.add_catalog("https://b.example.com/catalog.json")
def test_remove_catalog_wraps_yaml_parse_errors(self, tmp_path, monkeypatch):
"""Invalid YAML on disk surfaces as IntegrationValidationError from remove_catalog too."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
invalid_yaml = "catalogs:\n - url: 'https://a.example.com/cat.json'\n - [bad\n"
cfg_path.write_text(invalid_yaml, encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="Failed to read catalog config"
):
cat.remove_catalog(0)
def test_add_catalog_defaults_missing_priority_to_index_plus_one(
self, tmp_path, monkeypatch
):
"""Existing entries without `priority` should be treated as idx + 1.
Matches the rule in `_load_catalog_config()`: a valid catalog entry
without an explicit `priority` sorts at `idx + 1`, so the new entry
should get `max(...) + 1` from those derived values.
"""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
# No explicit priority → should be treated as 1
{"url": "https://a.example.com/cat.json", "name": "a"},
# No explicit priority → should be treated as 2
{"url": "https://b.example.com/cat.json", "name": "b"},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://c.example.com/cat.json", name="c")
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
new_entry = data["catalogs"][-1]
assert new_entry["name"] == "c"
# max(implicit [1, 2]) + 1 == 3
assert new_entry["priority"] == 3
def test_add_catalog_strips_whitespace_in_url(self, tmp_path, monkeypatch):
"""Whitespace around the incoming URL should be normalized before write."""
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog(" https://a.example.com/catalog.json\n", name="a")
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert data["catalogs"][0]["url"] == "https://a.example.com/catalog.json"
def test_add_catalog_rejects_whitespace_only_duplicate(self, tmp_path, monkeypatch):
"""A second add with only whitespace differences must be rejected as a duplicate."""
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://a.example.com/catalog.json", name="a")
with pytest.raises(IntegrationValidationError, match="already configured"):
cat.add_catalog(" https://a.example.com/catalog.json ")
def test_remove_catalog_wraps_unlink_oserror(self, tmp_path, monkeypatch):
"""An OSError from `Path.unlink` surfaces as IntegrationValidationError."""
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://only.example.com/catalog.json", name="only")
from pathlib import Path as _Path
def boom(self, *args, **kwargs):
raise OSError("simulated unlink failure")
monkeypatch.setattr(_Path, "unlink", boom)
with pytest.raises(
IntegrationValidationError, match="Failed to delete catalog config"
):
cat.remove_catalog(0)
def test_remove_catalog_ignores_missing_final_config_during_unlink(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://only.example.com/catalog.json", name="only")
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
from pathlib import Path as _Path
original_unlink = _Path.unlink
def delete_first_then_unlink(self, *args, **kwargs):
if self == cfg_path and self.exists():
original_unlink(self)
return original_unlink(self, *args, **kwargs)
monkeypatch.setattr(_Path, "unlink", delete_first_then_unlink)
assert cat.remove_catalog(0) == "only"
assert not cfg_path.exists()
def test_remove_catalog_empty_list_gives_clear_error(self, tmp_path, monkeypatch):
"""Hand-edited empty `catalogs:` produces a clear error, not '0--1'."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(yaml.dump({"catalogs": []}), encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="contains no catalog entries"
):
cat.remove_catalog(0)
def test_remove_catalog_empty_config_file_gives_clear_error(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text("", encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="contains no catalog entries"
):
cat.remove_catalog(0)
def test_remove_catalog_rejects_non_list_catalogs_with_config_path(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump({"catalogs": "not-a-list"}), encoding="utf-8"
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="invalid 'catalogs' value"
) as exc_info:
cat.remove_catalog(0)
assert str(cfg_path) in str(exc_info.value)
@pytest.mark.parametrize("config_content", ["[]\n", "false\n", "0\n", "''\n"])
def test_remove_catalog_rejects_falsy_non_mapping_config_roots(
self, tmp_path, monkeypatch, config_content
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(config_content, encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError,
match="corrupted.*expected a mapping",
) as exc_info:
cat.remove_catalog(0)
assert str(cfg_path) in str(exc_info.value)
def test_remove_last_catalog_deletes_file_and_restores_defaults(
self, tmp_path, monkeypatch
):
"""Removing the final catalog must not leave behind `catalogs: []`.
`_load_catalog_config` treats an empty `catalogs` list as an error,
so writing that file would break every subsequent `integration`
command. Removing the last entry should delete the config file so the
project falls back to built-in defaults.
"""
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cat.add_catalog("https://only.example.com/catalog.json", name="only")
assert cfg_path.exists()
assert [e.name for e in cat.get_active_catalogs()] == ["only"]
removed = cat.remove_catalog(0)
assert removed == "only"
assert not cfg_path.exists(), (
"remove_catalog should delete the config file when emptying it"
)
# Follow-up loads fall back to built-in defaults, not an error.
active = cat.get_active_catalogs()
assert [e.name for e in active] == ["default", "community"]
def test_load_catalog_config_raises_validation_error_for_invalid_yaml(
self, tmp_path, monkeypatch
):
"""Local-config problems must surface as IntegrationValidationError so
CLI handlers can route them to local-config (not network) guidance."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
invalid_yaml = "catalogs:\n - [bad\n"
cfg_path.write_text(invalid_yaml, encoding="utf-8")
cat = IntegrationCatalog(tmp_path)
# Subclass match: IntegrationValidationError (specifically), not the
# bare IntegrationCatalogError parent that callers used previously.
with pytest.raises(IntegrationValidationError, match="Failed to read catalog config"):
cat.get_active_catalogs()
def test_load_catalog_config_rejects_boolean_priority(self, tmp_path, monkeypatch):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{
"url": "https://a.example.com/catalog.json",
"name": "a",
"priority": True,
}
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError, match="Invalid priority|expected integer"
) as exc_info:
cat.get_active_catalogs()
assert str(cfg_path) in str(exc_info.value)
@pytest.mark.parametrize("raw_name", [None, " "])
def test_load_catalog_config_defaults_blank_names(
self, tmp_path, monkeypatch, raw_name
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{
"url": " ",
"name": "skipped",
},
{
"url": "https://example.com/catalog.json",
"name": raw_name,
}
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
assert [entry.name for entry in cat.get_active_catalogs()] == ["catalog-1"]
@pytest.mark.parametrize(
("raw_name", "expected"),
[
(None, "https://one.example.com/c.json"),
(" ", "https://one.example.com/c.json"),
(123, "123"),
],
)
def test_remove_catalog_normalizes_removed_display_name(
self, tmp_path, monkeypatch, raw_name, expected
):
self._isolate(tmp_path, monkeypatch)
cat = IntegrationCatalog(tmp_path)
cat.add_catalog("https://one.example.com/c.json", name="one")
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
data["catalogs"][0]["name"] = raw_name
cfg_path.write_text(yaml.dump(data), encoding="utf-8")
assert cat.remove_catalog(0) == expected
def test_remove_catalog_uses_display_order_with_explicit_priorities(
self, tmp_path, monkeypatch
):
"""`remove_catalog(index)` must remove the entry shown at that index by
`catalog list`, not the entry at that raw YAML position."""
self._isolate(tmp_path, monkeypatch)
# YAML order: alpha (priority=20), beta (priority=10), gamma (priority=15).
# Display (sorted by priority asc): beta (10), gamma (15), alpha (20).
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": "https://alpha.example.com/c.json", "name": "alpha", "priority": 20},
{"url": "https://beta.example.com/c.json", "name": "beta", "priority": 10},
{"url": "https://gamma.example.com/c.json", "name": "gamma", "priority": 15},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
# Display index 0 = beta (lowest priority), not alpha (raw YAML idx 0).
removed = cat.remove_catalog(0)
assert removed == "beta"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
remaining_names = [c["name"] for c in data["catalogs"]]
# YAML order is preserved for the survivors; only beta is gone.
assert remaining_names == ["alpha", "gamma"]
def test_remove_catalog_display_order_with_missing_priorities(
self, tmp_path, monkeypatch
):
"""Entries without `priority` default to `idx + 1` (matching
`_load_catalog_config`), so display order tracks YAML order and the
first display entry is the first YAML entry."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": "https://one.example.com/c.json", "name": "one"},
{"url": "https://two.example.com/c.json", "name": "two"},
{"url": "https://three.example.com/c.json", "name": "three"},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
# Implicit priorities: one=1, two=2, three=3 → display order matches YAML.
removed = cat.remove_catalog(0)
assert removed == "one"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert [c["name"] for c in data["catalogs"]] == ["two", "three"]
def test_remove_catalog_bool_priority_falls_back_to_yaml_index(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": "https://one.example.com/c.json", "name": "one"},
{
"url": "https://bool.example.com/c.json",
"name": "bool",
"priority": False,
},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
removed = cat.remove_catalog(0)
assert removed == "one"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert [c["name"] for c in data["catalogs"]] == ["bool"]
def test_remove_catalog_display_order_skips_blank_url_entries(
self, tmp_path, monkeypatch
):
"""Blank-url entries are not shown by catalog list, so remove skips them too."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": " ", "name": "blank", "priority": 0},
{"url": "https://one.example.com/c.json", "name": "one"},
{"url": "https://two.example.com/c.json", "name": "two"},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
removed = cat.remove_catalog(0)
assert removed == "one"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert [c["name"] for c in data["catalogs"]] == ["blank", "two"]
def test_remove_catalog_deletes_file_when_only_skipped_entries_remain(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": " ", "name": "blank", "priority": 0},
{"url": "https://one.example.com/c.json", "name": "one"},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
removed = cat.remove_catalog(0)
assert removed == "one"
assert not cfg_path.exists()
active = cat.get_active_catalogs()
assert [e.name for e in active] == ["default", "community"]
def test_remove_catalog_allows_numeric_url_entry_cleanup(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump({"catalogs": [{"name": "numeric-url", "url": 123}]}),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
removed = cat.remove_catalog(0)
assert removed == "numeric-url"
assert not cfg_path.exists()
def test_remove_catalog_errors_when_no_entries_are_removable(
self, tmp_path, monkeypatch
):
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": "", "name": "empty"},
{"name": "missing"},
"not-a-mapping",
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
with pytest.raises(
IntegrationValidationError,
match="no removable catalog entries",
):
cat.remove_catalog(0)
def test_remove_catalog_display_order_mixes_explicit_and_default(
self, tmp_path, monkeypatch
):
"""An explicit low priority should sort ahead of default-priority
siblings, even if it appears later in the YAML."""
self._isolate(tmp_path, monkeypatch)
cfg_path = tmp_path / ".specify" / "integration-catalogs.yml"
cfg_path.parent.mkdir(parents=True, exist_ok=True)
# Defaults: a=1, b=2 (implicit). Explicit c=0 → display: c, a, b.
# The blank name should fall back to the removed URL, not raw YAML idx.
cfg_path.write_text(
yaml.dump(
{
"catalogs": [
{"url": "https://a.example.com/c.json", "name": "a"},
{"url": "https://b.example.com/c.json", "name": "b"},
{
"url": "https://c.example.com/c.json",
"name": " ",
"priority": 0,
},
]
}
),
encoding="utf-8",
)
cat = IntegrationCatalog(tmp_path)
removed = cat.remove_catalog(0)
assert removed == "https://c.example.com/c.json"
data = yaml.safe_load(cfg_path.read_text(encoding="utf-8"))
assert [c["name"] for c in data["catalogs"]] == ["a", "b"]

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