Compare commits

...
21 Commits
Author SHA1 Message Date
github-actions[bot] b85aaeda4a chore: bump version to 0.16.3 2026-08-13 15:37:04 +00:00
Quratulain-bilalandGitHub 16f45774a5 fix: narrow bare except Exception in VS Code settings merge (#3844)
* fix: narrow bare except Exception in VS Code settings merge

Replace overly broad except Exception with (OSError, ValueError, KeyError)
to let programming errors like TypeError or AttributeError propagate while
still handling expected I/O and parse errors gracefully.

* test: verify programming errors propagate through handle_vscode_settings

The narrow exception change from 'except Exception' to
'except (OSError, ValueError, KeyError)' was not covered by a
regression test. Add a test that monkeypatches merge_json_files to
raise TypeError and verifies it propagates rather than being swallowed.
2026-08-13 10:36:00 -05:00
229022943c feat(presets): list presets in resolution/precedence order (#4086) (#4104)
`specify preset list` now sorts installed presets by (priority, id) so the
printed order matches the actual resolution/composition order used by
PresetRegistry.list_by_priority(). Lower priority number = higher precedence;
ties are broken alphabetically by preset id. Adds a header and footer note
clarifying the ordering, updates the presets reference docs, and adds tests.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2026-08-13 10:00:16 -05:00
e79fa25f3f Fix: scaffold self-contained namespaced preset commands (#4076) (#4082)
* Fix: scaffold self-contained namespaced preset commands (#4076)

Preset command templates named `speckit.<ns>.<cmd>` were silently dropped
whenever `.specify/extensions/<ns>/` was absent, while `speckit.<cmd>`
always scaffolded. The `_extension_installed_for_command` guard filtered
purely on name shape, conflating "override of an installed extension's
command" with "a preset shipping its own namespaced command." Because a
`type: command` template always ships its own body, such a command is
self-contained and must scaffold like any short-named command.

Remove the name-shape guard at all four call sites (registration, both
reconciliation passes, and skills). The reconciliation loop already skips
names that resolve to no layers (`if not layers: continue`), and the
composed-None branch still cleans up commands whose base layer disappeared.
Convert the command-mode "no base layer to compose onto" hard error into a
warn + skip, matching the existing behavior in _reconcile_composed_commands
so command-mode install and reconciliation stay consistent.

Update the two tests that encoded the old drop behavior to assert the new
consistent-scaffold contract, and add coverage proving 2-part and 3-part
preset commands scaffold identically with no extension installed.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cfc4f1ce-6acb-465a-aa7b-999f2e4197fb

* Skip uncomposable commands in skills mode too (PR #4082 review)

When _register_commands skips an uncomposable composition command (a
wrap/prepend/append with no base layer to compose onto — e.g. the command
it wraps comes from an uninstalled extension), install still passed the
full manifest to _register_skills. For a command-backed integration in
skills mode, _register_skills created the missing skill and fell back to
the raw preset body because no `.composed` file existed, materializing a
broken SKILL.md — a literal `{CORE_TEMPLATE}` for wrap, or just the
preset's own fragment for prepend/append. Previously the raise in
_register_commands aborted before skills ran, so this never surfaced.

Make _register_skills apply the same skip: for a composition-strategy
command with no `.composed` file, resolve the stack and skip when no base
exists (resolve_content is None). The skip is silent because
_register_commands already warned for the same command in the same pass.

Add a regression test proving an uncomposable wrap command renders no skill
and never leaks a literal {CORE_TEMPLATE} in skills mode.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cfc4f1ce-6acb-465a-aa7b-999f2e4197fb

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cfc4f1ce-6acb-465a-aa7b-999f2e4197fb
2026-08-13 09:04:39 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
750ce47cb8 Update Cross-Platform Governance preset to v0.2.2 (#4080)
Update cross-platform-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4078

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-13 08:36:36 -05:00
197dde6253 fix(bundler): treat a blank active integration as indeterminate in FR-019 (#3886)
`resolve_install_plan`'s two FR-019 guards are a truthiness test and an
`is None` test:

    if active_integration and required != active_integration:   # clash
    if active_integration is None and not integration_explicit: # indeterminate

An empty string satisfies neither, so it falls through to
`effective_integration = required` and the bundle's pinned integration is
silently adopted — the exact outcome the docstring says the guard prevents
("resolution fails instead of silently adopting the bundle's required
integration").

  active=None        -> BundlerError: ... could not be determined
  active='' (blank)  -> effective_integration='copilot'   <-- silent adopt
  active='claude'    -> BundlerError: ... targets integration 'copilot'

Normalise a blank value to None before the guards, and strip first to
match the writer (`integration_state.clean_integration_key`, which returns
`None` for empty/whitespace and strips otherwise) so a padded value is not
reported as clashing with its own unpadded form.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 11:52:34 -05:00
Luca BottiandGitHub f2583e675c Integrate Junie with dot-to-hyphen behavior and command formatting (#4073)
* Add Junie integration with dot-to-hyphen behavior, command formatting, and file transformations. Based on Cline Integration.

* Fix references to Cline in Junie integration and update class/test names for consistency.

* Fix references to Cline in Junie integration and update class/test names for consistency.

* Modified to generate correct formatting in junie
2026-08-12 11:36:12 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
996b24c561 Update A11Y Governance preset to v0.4.3 (#4074)
Update a11y-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, templates count, tags)
- docs/community/presets.md community presets table

Closes #4064


Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-12 11:34:40 -05:00
b77ca572ca Fix Alquimia argument hints after folded descriptions (#4063)
Co-authored-by: root <kinsonnee@gmail.com>
2026-08-12 11:13:02 -05:00
Quratulain-bilalandGitHub c1bceb625c fix: use bounded read for bundle download HTTP responses (#3764)
* fix: use bounded read for bundle download HTTP responses

The bundle download used unbounded resp.read() to read HTTP responses
into memory. A malicious or misconfigured catalog server could return
an arbitrarily large payload causing OOM.

Replace with read_response_limited() capped at MAX_DOWNLOAD_BYTES
(50 MiB), consistent with how other download paths in the codebase
enforce bounded reads.

Add regression test that monkeypatches MAX_DOWNLOAD_BYTES to 100 bytes
and verifies oversized responses are rejected.

* fix: remove duplicate import of MAX_DOWNLOAD_BYTES and read_response_limited
2026-08-12 08:29:36 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
7dd706880e Update iSAQB Architecture Governance preset to v0.2.2 (#4056)
Update isaqb-architecture-governance preset submitted by @hindermath:
- presets/catalog.community.json (version, download_url, documentation, description, templates count, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4055

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-11 16:58:14 -05:00
chelsealongandGitHub bd595cf838 fix(claude): make argument-hint injection fold-aware for long descriptions (#4045)
* fix(claude): make argument-hint injection fold-aware for long descriptions

ClaudeIntegration.inject_argument_hint spliced argument-hint: "..." as a
raw text line right after the first line starting with "description:".
When a description is long enough for the YAML dumper to fold it across
indented continuation lines, that splice landed inside the scalar,
producing invalid YAML (plain scalar) or silently absorbing the hint
into the description string (quoted scalar). This reproduces #3991 for
the case #3996 didn't cover: bundled core commands have no argument-hint
in their source frontmatter, so the structural apply_argument_hint path
is a no-op and this raw-text fallback is what actually runs.

Skip every continuation line of the description scalar (anything more
indented than the key itself) before inserting, so the new key always
lands after the whole scalar ends rather than in the middle of it.

Fixes #4044

* fix(claude): also skip unindented blank lines in description scalar

PyYAML serializes an embedded paragraph break ("\n\n") inside a quoted
description as unindented blank lines, not indented continuation
lines. inject_argument_hint only skipped indented lines, so it still
inserted argument-hint mid-scalar for multi-paragraph descriptions,
reproducing the #4044 failure modes. Skip blank lines too, and add a
regression test for the multi-paragraph case.
2026-08-11 13:14:19 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
85d3ed289d Add SpecKit Grill Me extension to community catalog (#4052)
Add grill extension submitted by @yoshi1220 to:
- extensions/catalog.community.json (alphabetical order)
- docs/community/extensions.md community extensions table

Closes #4047

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-11 13:12:48 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
5ec937e07a Update Architecture Governance preset to v0.5.2 (#4050)
Update architecture-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, documentation, description, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4042

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-11 11:17:33 -05:00
6461f09483 Remove auto-assign from catalog submission workflow (#4054)
Catalog submission issues no longer auto-assign mnriem. The workflow now
only posts the team notification comment (cc @github/spec-kit-maintainers),
since GitHub issues cannot be assigned to a team. Renamed the workflow and
job to reflect its notification-only purpose.

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, supervised)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 63ae7e23-13e3-4a83-96b3-1178422aaa6e
2026-08-11 11:16:53 -05:00
fb427e47ef docs: clarify example spec guidance (#4048)
Co-authored-by: root <kinsonnee@gmail.com>
2026-08-11 11:11:30 -05:00
bd04776dcc Clarify custom checklist ownership and lifecycle (#4028)
* docs: clarify custom checklist lifecycle

* docs: clarify implement checklist marker ownership

---------

Co-authored-by: root <kinsonnee@gmail.com>
2026-08-11 11:09:56 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
c5258e680b Update Archive Extension to v1.2.2 (#4053)
Update archive extension submitted by @stn1slv:
- extensions/catalog.community.json (version, download_url, updated_at)
- docs/community/extensions.md community extensions table

Closes #4049

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-11 10:29:58 -05:00
github-actions[bot]GitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>Copilot
9d15554c08 Update Security Governance preset to v0.6.2 (#4040)
Update security-governance preset submitted by @hindermath to:
- presets/catalog.community.json (version, download_url, description, documentation, provides, tags, updated_at)
- docs/community/presets.md community presets table

Closes #4039

Assisted-by: GitHub Copilot (model: claude-sonnet-4.6, autonomous)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-10 16:22:36 -05:00
c3bbcc40a0 docs: clarify maintainer applies submission label during triage (#4041)
Community extension, preset, and bundle submissions are validated by
label-triggered agentic workflows that only run once the corresponding
`*-submission` label is applied. On this public repo contributors cannot
apply that label themselves, so a maintainer applies it during issue
triage. Document this in the three submission issue templates, the preset
publishing guide, and correct the extension guide's inaccurate claim that
issues are "automatically labeled and assigned".

Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 3b6f6854-9be6-4b67-b5b9-34b19ededcb8
2026-08-10 15:37:51 -05:00
Manfred RiemGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
39b56626c9 chore: release 0.16.2, begin 0.16.3.dev0 development (#4038)
* chore: bump version to 0.16.2

* chore: begin 0.16.3.dev0 development

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-10 14:46:12 -05:00
36 changed files with 1194 additions and 256 deletions
@@ -16,6 +16,8 @@ body:
- If you host a bundle catalog, test catalog installation with `specify bundle catalog add <catalog-url> --id <catalog-id> --policy install-allowed` and `specify bundle install <bundle-id>`
- If your bundle depends on components from non-default catalogs, document those catalog URLs and test installation from a clean project
**After submitting:** a maintainer applies the `bundle-submission` label during issue triage, which starts the automated catalog validation. You don't need to apply any label or ask for one.
- type: input
id: bundle-id
attributes:
@@ -14,6 +14,8 @@ body:
- Create a GitHub release with a version tag (e.g., v1.0.0)
- Test installation: `specify extension add <extension-name> --from <your-release-url>`
**After submitting:** a maintainer applies the `extension-submission` label during issue triage, which starts the automated catalog validation. You don't need to apply any label or ask for one.
- type: input
id: extension-id
attributes:
@@ -14,6 +14,8 @@ body:
- Create a GitHub release with a version tag (e.g., v1.0.0)
- Test installation from the release archive: `specify preset add --from <download-url>`
**After submitting:** a maintainer applies the `preset-submission` label during issue triage, which starts the automated catalog validation. You don't need to apply any label or ask for one.
- type: input
id: preset-id
attributes:
+2 -18
View File
@@ -1,11 +1,11 @@
name: "Catalog: Auto-assign submission"
name: "Catalog: Notify submission"
on:
issues:
types: [opened, labeled]
jobs:
assign:
notify:
if: >
(github.event.action == 'opened' && (
contains(github.event.issue.labels.*.name, 'extension-submission') ||
@@ -24,24 +24,8 @@ jobs:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
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,
+50
View File
@@ -2,6 +2,56 @@
<!-- insert new changelog below this comment -->
## [0.16.3] - 2026-08-13
### Changed
- fix: narrow bare except Exception in VS Code settings merge (#3844)
- feat(presets): list presets in resolution/precedence order (#4086) (#4104)
- Fix: scaffold self-contained namespaced preset commands (#4076) (#4082)
- Update Cross-Platform Governance preset to v0.2.2 (#4080)
- fix(bundler): treat a blank active integration as indeterminate in FR-019 (#3886)
- Integrate Junie with dot-to-hyphen behavior and command formatting (#4073)
- Update A11Y Governance preset to v0.4.3 (#4074)
- Fix Alquimia argument hints after folded descriptions (#4063)
- fix: use bounded read for bundle download HTTP responses (#3764)
- Update iSAQB Architecture Governance preset to v0.2.2 (#4056)
- fix(claude): make argument-hint injection fold-aware for long descriptions (#4045)
- Add SpecKit Grill Me extension to community catalog (#4052)
- Update Architecture Governance preset to v0.5.2 (#4050)
- Remove auto-assign from catalog submission workflow (#4054)
- docs: clarify example spec guidance (#4048)
- Clarify custom checklist ownership and lifecycle (#4028)
- Update Archive Extension to v1.2.2 (#4053)
- Update Security Governance preset to v0.6.2 (#4040)
- docs: clarify maintainer applies submission label during triage (#4041)
- chore: release 0.16.2, begin 0.16.3.dev0 development (#4038)
## [0.16.2] - 2026-08-10
### Changed
- Add Command Code integration to spec-kit (#4019)
- fix(workflows): strip a resolved condition before the true/false check (#3883)
- fix(workflows): guard a non-string overlay edit 'operation' (#3881)
- fix: bound response read in integration catalog fetch (#3818)
- Fix bug-test Python dependency provisioning (#4030)
- fix(bundle): escape Rich markup in bundle CLI error and status output (#4023)
- fix(presets): skip an unreadable restore source in `preset remove` (#4020)
- Add Keel Discovery extension to community catalog (#4035)
- fix: show error details in preset catalog config read failure (#3840)
- Update Reconcile Extension to v1.1.0 (#4034)
- Add Model Routing Governance preset to community catalog (#4033)
- fix: use missing_ok=True in integration JSON removal (#3846)
- fix: use missing_ok=True in extension cache clear (#3845)
- fix(extensions): reject duplicate provides.templates/scripts names (#4016)
- feat(presets): resolve constitution templates at command time (#3984)
- [bug-fix] Fix preset-wrap-drops-argument-hint: inherit argument-hint from core template (#3996)
- docs: document installing specify-cli from a custom package index (#4032)
- feat(extensions): accept provides.templates and provides.scripts in manifest (#4012)
- fix(presets): treat an unreadable core template as missing (#3961)
- chore: release 0.16.1, begin 0.16.2.dev0 development (#4014)
## [0.16.1] - 2026-08-07
### Changed
+4 -2
View File
@@ -283,9 +283,11 @@ specify bundle validate --path ./my-bundle # structural + reference checks
specify bundle build --path ./my-bundle # produce a versioned .zip artifact
```
Four ready-to-read example manifests live under
Four ready-to-read example bundle manifests live under
[`examples/bundles/`](examples/bundles/) (product manager, business analyst,
security researcher, developer).
security researcher, developer). These are bundle packaging examples, not
filled generated feature specs; for end-to-end community examples, see the
[community walkthroughs](https://github.github.io/spec-kit/community/walkthroughs.html).
Key guarantees: `info` shows exactly what `install` adds (transparency);
installs are idempotent and confined to the project root; `remove` never touches
+2 -1
View File
@@ -34,7 +34,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Architect Impact Previewer | Predicts architectural impact, complexity, and risks of proposed changes before implementation. | `visibility` | Read-only | [spec-kit-architect-preview](https://github.com/UmmeHabiba1312/spec-kit-architect-preview) |
| Architecture Guard | Framework-agnostic architecture review extension for validating implementation against governance and architecture constitutions, detecting architectural drift, and generating non-blocking refactor tasks | `process` | Read+Write | [spec-kit-architecture-guard](https://github.com/DyanGalih/spec-kit-architecture-guard) |
| Architecture Workflow | Generate or reverse project-level 4+1 architecture views with per-view and full-workflow commands | `docs` | Read+Write | [spec-kit-arch](https://github.com/bigsmartben/spec-kit-arch) |
| Archive Extension | Archive merged features into main project memory. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
| Archive Extension | Archive merged features into main project memory, resolving gaps and conflicts. | `docs` | Read+Write | [spec-kit-archive](https://github.com/stn1slv/spec-kit-archive) |
| Azure DevOps Integration | Sync user stories and tasks to Azure DevOps work items using OAuth authentication | `integration` | Read+Write | [spec-kit-azure-devops](https://github.com/pragya247/spec-kit-azure-devops) |
| Blueprint | Stay code-literate in AI-driven development: review a complete code blueprint for every task from spec artifacts before /speckit.implement runs | `docs` | Read+Write | [spec-kit-blueprint](https://github.com/chordpli/spec-kit-blueprint) |
| Blueprint Index — Living Architecture Map | A living architecture map for spec-driven projects, kept honest by a deterministic, low-friction, machine-first CI gate (JSON, self-healable) that blocks only when the map contradicts the specs or code. Brownfield or greenfield. | `process` | Read+Write | [spec-kit-blueprint](https://github.com/ogil109/spec-kit-blueprint) |
@@ -146,6 +146,7 @@ The following community-contributed extensions are available in [`catalog.commun
| Spec-Kit BDD | ATDD/BDD extension: convert specs to Gherkin scenarios, scaffold step definitions, and verify acceptance test coverage | `process` | Read+Write | [spec-kit-bdd](https://github.com/RSginer/spec-kit-bdd) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure | `process` | Read+Write | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| SpecKit Companion | Live spec-driven progress — lifecycle capture, status, resume, and a turbo pipeline profile | `visibility` | Read+Write | [speckit-companion](https://github.com/alfredoperez/speckit-companion) |
| SpecKit Grill Me | Exhaustively resolve specification ambiguities and decisions before planning | `process` | Read+Write | [speckit-grill-me](https://github.com/yoshi1220/speckit-grill-me) |
| 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) |
+5 -5
View File
@@ -7,22 +7,22 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Preset | Purpose | Provides | Requires | URL |
|--------|---------|----------|----------|-----|
| A11Y Governance | Adds accessibility (WCAG 2.2 AA), accessible text and JSON status parity, bilingual DE/EN delivery, CEFR-B2 readability, inclusive-content governance, didactic inline-code-comment review, and audit-ready Spec-Kit run evidence to Spec Kit | 10 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) |
| A11Y Governance | Adds WCAG 2.2 AA, accessible status output, bilingual CEFR-B2 delivery, inclusive-content and didactic-comment governance, and provider-neutral model routing. | 11 templates, 3 commands | — | [spec-kit-preset-a11y-governance](https://github.com/hindermath/spec-kit-preset-a11y-governance) |
| Agent Parity Governance | Adds shared-guidance and generated-command parity, repository-fleet completion evidence, secret-free runner/status metadata, audit-ready Spec-Kit run evidence, and agent-neutral model-routing guidance across declared AI-agent surfaces. | 6 templates, 3 commands | — | [spec-kit-preset-agent-parity-governance](https://github.com/hindermath/spec-kit-preset-agent-parity-governance) |
| AIDE In-Place Migration | Adapts the AIDE extension workflow for in-place technology migrations (X → Y pattern) — adds migration objectives, verification gates, knowledge documents, and behavioral equivalence criteria | 2 templates, 8 commands | AIDE extension | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Architecture Governance | Adds secure software architecture, resumable remote-transaction boundaries, STRIDE+CAPEC threat modeling, arc42 security cross-cutting concepts, S-ADRs, Zero Trust applicability, OWASP SAMM governance, BSI C3A cloud autonomy, BSI C5 cloud compliance assurance, and audit-ready Spec Kit run evidence | 13 templates, 3 commands | — | [spec-kit-preset-architecture-governance](https://github.com/hindermath/spec-kit-preset-architecture-governance) |
| Architecture Governance | Adds secure architecture, STRIDE/CAPEC threat modeling, arc42/S-ADR guidance, Zero Trust, SAMM, BSI cloud assurance, audit evidence, and provider-neutral model routing. | 14 templates, 3 commands | — | [spec-kit-preset-architecture-governance](https://github.com/hindermath/spec-kit-preset-architecture-governance) |
| Autonomous Run Governance | Adds permission-bounded autonomous delivery, an optional intake-review gate, and preservation of the project's learner and accessibility contract. | 13 templates, 5 commands, 4 scripts | — | [spec-kit-preset-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-autonomous-run-governance) |
| Canon Core | Adapts original Spec Kit workflow to work together with Canon extension | 2 templates, 8 commands | — | [spec-kit-canon](https://github.com/maximiliamus/spec-kit-canon) |
| Claude AskUserQuestion | Upgrades `/speckit.clarify` and `/speckit.checklist` on Claude Code from Markdown-table prompts to the native AskUserQuestion picker, with a recommended option and reasoning on every question | 2 commands | — | [spec-kit-preset-claude-ask-questions](https://github.com/0xrafasec/spec-kit-preset-claude-ask-questions) |
| Command Density | Compacts the nine core Spec Kit command prompts while preserving scripts, handoffs, placeholders, hook output blocks, and rule structure | 9 commands | — | [spec-kit-preset-command-density](https://github.com/Xopoko/spec-kit-preset-command-density) |
| Cross-Platform Governance | Adds Bash/PowerShell and read-only check parity, root-path and native-override review, Unix man pages, bilingual help, Verb-Noun discipline, and audit-ready evidence. | 8 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
| Cross-Platform Governance | Adds Bash/PowerShell parity, read-only checks, path and native-override review, Unix man pages, bilingual PowerShell help, and provider-neutral model routing. | 9 templates, 3 commands | — | [spec-kit-preset-cross-platform-governance](https://github.com/hindermath/spec-kit-preset-cross-platform-governance) |
| Explicit Task Dependencies | Adds explicit `(depends on T###)` dependency declarations and an Execution Wave DAG to tasks.md for parallel scheduling | 1 template, 1 command | — | [spec-kit-preset-explicit-task-dependencies](https://github.com/Quratulain-bilal/spec-kit-preset-explicit-task-dependencies) |
| Fiction Book Writing | It adapts the Spec-Driven Development workflow for storytelling to create books or audiobooks (with annotations) in 12 languages: features become story elements, specs become story briefs, plans become story structures, and tasks become scene-by-scene writing tasks. Supports single and multi-POV, all major plot structure frameworks, and two style modes: an author voice sample or humanized AI prose principles. Supports interactive elements like brainstorming, interview, roleplay, and extras like statistics, cover builder, illustration builder, and bio command. Export with templates for KDP, D2D, etc. | 26 templates, 34 commands, 2 scripts | — | [speckit-preset-fiction-book-writing](https://github.com/adaumann/speckit-preset-fiction-book-writing) |
| Game Narrative Writing | Preset for game narrative design and interactive storytelling. It adapts the Spec-Driven Development workflow for game narratives: features become story mechanics, specs become narrative briefs, plans become story maps, and tasks become dialogue and scene-writing tasks. Supports branching narratives, player agency systems, state machines, and interactive dialogue trees. | 37 templates, 34 commands, 5 scripts | — | [speckit-preset-game-narrative-writing](https://github.com/adaumann/speckit-preset-game-narrative-writing) |
| Intake Authoring Governance | Governs traceable intake CRUD and language-aware requirements collections with atomic migrations, rollback evidence, and safe series authoring. | 12 templates, 5 commands, 7 scripts | — | [spec-kit-preset-intake-authoring-governance](https://github.com/hindermath/spec-kit-preset-intake-authoring-governance) |
| Intake Review Governance | Reviews single, series, campaign, and language-aware requirements collections before Spec Kit execution. | 8 templates, 3 commands, 4 scripts | — | [spec-kit-preset-intake-review-governance](https://github.com/hindermath/spec-kit-preset-intake-review-governance) |
| Intake Sequencing Governance | Manages language-aware intake-series order, typed dependencies, lifecycle, and authority-neutral next-candidate selection. | 11 templates, 6 commands, 8 scripts | — | [spec-kit-preset-intake-sequencing-governance](https://github.com/hindermath/spec-kit-preset-intake-sequencing-governance) |
| iSAQB Architecture Governance | Adds iSAQB/CPSA-F and arc42 architecture governance with audit-ready evidence for goals, views, resumability, partial-failure scenarios, ADRs, risks, and technical debt. | 13 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
| iSAQB Architecture Governance | Adds iSAQB/CPSA-F and arc42 architecture governance, architecture views, quality scenarios, ADRs, risks, technical-debt evidence, and provider-neutral model routing. | 14 templates, 3 commands | — | [spec-kit-preset-isaqb-architecture-governance](https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance) |
| Jira Issue Tracking | Overrides `speckit.taskstoissues` to create Jira epics, stories, and tasks instead of GitHub Issues via Atlassian MCP tools | 1 command | — | [spec-kit-preset-jira](https://github.com/luno/spec-kit-preset-jira) |
| Model Driven Engineering | Focuses on streamlined commands, app repository support, cross-spec support, and capability-aware project memory for model-driven engineering workflows | 6 templates, 11 commands | MDE extension | [spec-kit-preset-mde](https://github.com/AI-MDE/spec-kit-preset-mde) |
| Model Routing Governance | Maps provider-neutral Spec Kit roles to validated harness-local runner profiles without storing model availability, credentials, or machine-specific selections in Git. | 4 templates, 2 commands, 2 scripts | — | [spec-kit-preset-model-routing-governance](https://github.com/hindermath/spec-kit-preset-model-routing-governance) |
@@ -30,7 +30,7 @@ The following community-contributed presets customize how Spec Kit behaves — o
| Parallel Autonomous Run Governance | Coordinates permission-bounded autonomous campaigns while preserving the project's learner and accessibility contract across workers and consolidation. | 9 templates, 5 commands, 2 scripts | autonomous-run-governance >=0.2.2; optional: intake-review-governance >=0.1.0 | [spec-kit-preset-parallel-autonomous-run-governance](https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance) |
| Pirate Speak (Full) | Transforms all Spec Kit output into pirate speak — specs become "Voyage Manifests", plans become "Battle Plans", tasks become "Crew Assignments" | 6 templates, 9 commands | — | [spec-kit-presets](https://github.com/mnriem/spec-kit-presets) |
| Screenwriting | Spec-Driven Development for screenwriting/scriptwriting/tutorials: feature films, television (pilot, episode, limited series), and stage plays. Adapts the Spec Kit workflow to screenplay craft — slug lines, action lines, act breaks, beat sheets, and industry-standard pitch documents. Supports three-act, Save the Cat, TV pilot, network episode, cable/streaming episode, and stage-play structural frameworks. Export to Fountain, FTX, PDF | 26 templates, 32 commands, 1 script | — | [speckit-preset-screenwriting](https://github.com/adaumann/speckit-preset-screenwriting) |
| Security Governance | Adds memory-safe-language and secure-coding governance, exact-head and security-gate evidence, provider-failure classification, ASVS, supply-chain transparency, and EU regulatory screening. | 14 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| Security Governance | Adds memory-safe-language and secure-coding governance, exact-head security evidence, ASVS, supply-chain transparency, EU regulatory screening, and provider-neutral model routing. | 15 templates, 3 commands | — | [spec-kit-preset-security-governance](https://github.com/hindermath/spec-kit-preset-security-governance) |
| SicarioSpec Core | Baseline secure-by-default Spec Kit governance profile. | 5 templates | — | [sicario-spec](https://github.com/dfirs1car1o/sicario-spec) |
| Spec2Cloud | Spec-driven workflow tuned for shipping to Azure: spec → plan → tasks → implement → deploy | 5 templates, 8 commands | — | [spec2cloud](https://github.com/Azure-Samples/Spec2Cloud) |
| Table of Contents Navigation | Adds a navigable Table of Contents to generated spec.md, plan.md, and tasks.md documents | 3 templates, 3 commands | — | [spec-kit-preset-toc-navigation](https://github.com/Quratulain-bilal/spec-kit-preset-toc-navigation) |
+1 -1
View File
@@ -3,7 +3,7 @@
> [!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:
See Spec-Driven Development in action across different scenarios with these community-contributed walkthroughs. They are useful read-only examples of completed flows, but they are not official golden outputs for generated specs, plans, or tasks.
- **[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.
+5 -5
View File
@@ -5,8 +5,7 @@ This guide will help you get started with Spec-Driven Development using Spec Kit
> [!NOTE]
> Automation scripts are provided as Bash (`.sh`), PowerShell (`.ps1`), and Python (`.py`) variants. Interactive `specify init` prompts you to choose one; non-interactive runs default to a shell variant for your OS. Pass `--script sh|ps|py` to select explicitly.
> [!NOTE]
> Commands are shown here in `/speckit.*` form, but the exact invocation depends on your agent. Some skills-based agents use `$speckit-*` (e.g. Codex, ZCode) or `/skill:speckit-*` (e.g. Kimi). Use whichever form your agent exposes — the steps are otherwise identical.
Commands are shown here in `/speckit.*` form, but the exact invocation depends on your agent. Some skills-based agents use `$speckit-*` (e.g. Codex, ZCode) or `/skill:speckit-*` (e.g. Kimi). Use whichever form your agent exposes — the steps are otherwise identical.
## Recommended Process
@@ -83,7 +82,7 @@ Generates the design artifacts from the spec. This is where implementation detai
### Step 5: `/speckit.checklist` — validate the spec
Generates a quality checklist — "unit tests for your requirements" — to confirm the spec is complete, clear, and consistent before you break the work down.
Generates a custom quality checklist — "unit tests for your requirements" — to confirm the spec is complete, clear, and consistent before you break the work down. These custom checklists are reviewer-owned requirements-quality review artifacts: mark an item `[x]` only when the reviewer determines that requirement-quality criterion is satisfied. Checked custom items do not mean implementation work is complete.
```text
/speckit.checklist
@@ -107,7 +106,7 @@ Reports conflicts, gaps, and ambiguities across `spec.md`, `plan.md`, and `tasks
### Step 8: `/speckit.implement` — build it
Executes the tasks in `tasks.md` in dependency order. Run it once to build everything, or scope it to one phase at a time for large features.
Executes the tasks in `tasks.md` in dependency order. Before implementation, it reads checklist checkbox state as a gate and asks before proceeding if any checklist items are unchecked; it does not change any checklist files or markers. The built-in `checklists/requirements.md` checklist is maintained by `/speckit.specify` and `/speckit.clarify`, while custom checklists remain reviewer-owned. Run it once to build everything, or scope it to one phase at a time for large features.
```text
/speckit.implement
@@ -136,5 +135,6 @@ Checks the codebase against the spec, plan, and tasks. If it finds gaps, it appe
- See the [Agentic SDD](reference/agentic-sdd.md) reference for full detail on every command
- Read the [complete methodology](https://github.com/github/spec-kit/blob/main/spec-driven.md) for in-depth guidance
- Check out [more examples](https://github.com/github/spec-kit/tree/main/templates) in the repository
- Compare the [core templates](https://github.com/github/spec-kit/tree/main/templates) with
[community walkthroughs](community/walkthroughs.md) to see how Spec-Driven Development is used in real projects
- Explore the [source code on GitHub](https://github.com/github/spec-kit)
+9 -1
View File
@@ -23,6 +23,8 @@ Creates or updates the project **constitution** — the guiding principles that
Creates or updates the feature **specification** from a natural-language description. Focus on the **what** and **why** — the user-facing behavior and goals — not the tech stack, which belongs in `/speckit.plan`.
This workflow may also maintain `checklists/requirements.md`, the built-in spec-quality checklist that `/speckit.specify` creates and `/speckit.clarify` re-evaluates. That lifecycle is separate from custom checklists generated by `/speckit.checklist`.
```text
/speckit.specify Build an application that helps me organize photos into albums grouped by date, re-orderable by drag-and-drop on the main page, with a tile preview inside each album.
```
@@ -37,6 +39,8 @@ Asks up to five targeted questions about underspecified areas of the current spe
Clarifying before planning keeps you from designing on top of ambiguity. If `/speckit.analyze` later surfaces requirement gaps, come back and run `/speckit.clarify` (or `/speckit.specify`) again.
When `checklists/requirements.md` exists, `/speckit.clarify` may update its evaluated state as part of tightening the spec. This exception applies only to the built-in requirements checklist, not to custom review checklists.
## `/speckit.plan`
Runs the planning process to generate design artifacts from the spec. This is where implementation detail belongs — provide your tech stack, architecture, and technical constraints as arguments.
@@ -49,6 +53,8 @@ Runs the planning process to generate design artifacts from the spec. This is wh
Generates a quality checklist for the feature — think of it as **"unit tests for your requirements."** Rather than testing code, it checks whether the spec itself is complete, clear, unambiguous, and consistent (for example: "Are the drag-and-drop rules defined for every column?", "Is behavior specified for a deleted assigned user?").
Custom checklists generated by this command are reviewer-owned requirements-quality review artifacts. An agent may help evaluate them when explicitly asked, but implementation must not silently self-approve them. In a custom checklist, `[x]` means the reviewer determined the requirements-quality criterion is satisfied; it does not mean implementation work is complete.
Run it with no arguments for a broad pass, or pass a focus area to target one aspect:
```text
@@ -59,7 +65,7 @@ Run it with no arguments for a broad pass, or pass a focus area to target one as
/speckit.checklist Focus on the Kanban board interactions and comment permissions.
```
Review the generated checklist. If it surfaces gaps, loop back to `/speckit.clarify` or `/speckit.specify` to tighten the spec before breaking the work down.
Review the generated checklist. If it surfaces gaps, loop back to `/speckit.clarify` or `/speckit.specify` to tighten the spec before breaking the work down, then mark each custom checklist item `[x]` only after the requirements-quality criterion has been reviewed and satisfied.
## `/speckit.tasks`
@@ -83,6 +89,8 @@ Run it before implementing, while the artifacts can still be adjusted cheaply. I
Executes the tasks in `tasks.md`, running each phase in dependency order and respecting parallel markers.
Before executing tasks, it reads checklist checkbox state as a gate. Checklist markers are read-only for this command: `/speckit.implement` counts checked and unchecked items and asks before proceeding when any are unchecked, but it must not change checklist markers. For custom checklists, checked items mean reviewer approval of requirements quality, not completed implementation work.
For a small feature, run it once to build everything:
```text
+2
View File
@@ -47,6 +47,8 @@ specify preset list
Lists installed presets with their versions, descriptions, template counts, and current status.
Presets are printed in **resolution/precedence order**: the highest-precedence preset (lowest priority number) is listed first, and ties on priority are broken alphabetically by preset id. This matches the order used when composing commands and resolving templates, so the top entry is the one that wins for overlapping files.
## Preset Info
```bash
+1 -1
View File
@@ -151,7 +151,7 @@ To submit your extension to the community catalog, file a new issue using the **
### What Happens After You Submit
1. Your issue is automatically labeled and assigned to a maintainer for review
1. A maintainer reviews the issue during issue triage and applies the `extension-submission` label, which starts the automated catalog validation. On this public repository, contributors cannot apply that label themselves, so there is nothing to label or re-request — the issue simply waits in triage.
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`
+39 -4
View File
@@ -1,6 +1,6 @@
{
"schema_version": "1.0",
"updated_at": "2026-08-10T00:00:00Z",
"updated_at": "2026-08-11T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/extensions/catalog.community.json",
"extensions": {
"adrkit": {
@@ -362,8 +362,8 @@
"id": "archive",
"description": "Archive merged features into main project memory, resolving gaps and conflicts.",
"author": "Stanislav Deviatov",
"version": "1.1.0",
"download_url": "https://github.com/stn1slv/spec-kit-archive/archive/refs/tags/v1.1.0.zip",
"version": "1.2.2",
"download_url": "https://github.com/stn1slv/spec-kit-archive/archive/refs/tags/v1.2.2.zip",
"repository": "https://github.com/stn1slv/spec-kit-archive",
"homepage": "https://github.com/stn1slv/spec-kit-archive",
"documentation": "https://github.com/stn1slv/spec-kit-archive/blob/main/README.md",
@@ -388,7 +388,7 @@
"downloads": 0,
"stars": 0,
"created_at": "2026-03-14T00:00:00Z",
"updated_at": "2026-08-04T00:00:00Z"
"updated_at": "2026-08-11T00:00:00Z"
},
"azure-devops": {
"name": "Azure DevOps Integration",
@@ -1815,6 +1815,41 @@
"created_at": "2026-06-24T00:00:00Z",
"updated_at": "2026-07-07T00:00:00Z"
},
"grill": {
"name": "SpecKit Grill Me",
"id": "grill",
"description": "Exhaustively resolve specification ambiguities and decisions before planning.",
"author": "yoshi1220",
"version": "1.0.0",
"download_url": "https://github.com/yoshi1220/speckit-grill-me/releases/download/v1.0.0/speckit-grill-me-extension-v1.0.0.zip",
"repository": "https://github.com/yoshi1220/speckit-grill-me",
"homepage": "https://github.com/yoshi1220/speckit-grill-me/tree/main/spec-kit-extension",
"documentation": "https://github.com/yoshi1220/speckit-grill-me/blob/main/spec-kit-extension/README.md",
"changelog": "https://github.com/yoshi1220/speckit-grill-me/blob/main/spec-kit-extension/CHANGELOG.md",
"license": "MIT",
"category": "process",
"effect": "read-write",
"requires": {
"speckit_version": ">=0.16.2",
"tools": [{ "name": "bash", "required": true }]
},
"provides": {
"commands": 1,
"hooks": 0
},
"tags": [
"clarification",
"requirements",
"specification",
"elicitation",
"workflow"
],
"verified": false,
"downloads": 0,
"stars": 0,
"created_at": "2026-08-11T00:00:00Z",
"updated_at": "2026-08-11T00:00:00Z"
},
"harness": {
"name": "Research Harness",
"id": "harness",
+6
View File
@@ -300,6 +300,12 @@ git push origin add-your-preset
## Verification Process
> **How submissions get picked up:** the automated catalog-validation workflow only runs
> once the `preset-submission` label is on the issue. On this public repository, contributors
> cannot apply that label themselves — a maintainer applies it during issue triage. Until then
> the issue simply waits in triage; there is no action required from you, and there is no need to
> re-request the label in a comment.
After submission, maintainers will review:
1. **Manifest validation** — valid `preset.yml`, all files exist
+38 -84
View File
@@ -1,40 +1,35 @@
{
"schema_version": "1.0",
"updated_at": "2026-08-10T00:00:00Z",
"updated_at": "2026-08-12T00:00:00Z",
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/presets/catalog.community.json",
"presets": {
"a11y-governance": {
"name": "A11Y Governance",
"id": "a11y-governance",
"version": "0.4.2",
"description": "Adds accessibility (WCAG 2.2 AA), accessible text and JSON status parity, bilingual DE/EN delivery, CEFR-B2 readability, inclusive-content governance, didactic inline-code-comment review, and audit-ready Spec-Kit run evidence to Spec Kit.",
"version": "0.4.3",
"description": "Adds WCAG 2.2 AA, accessible status output, bilingual CEFR-B2 delivery, inclusive-content and didactic-comment governance, and provider-neutral model routing.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-a11y-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-a11y-governance/archive/refs/tags/v0.4.2.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-a11y-governance/archive/refs/tags/v0.4.3.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-a11y-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-a11y-governance/blob/v0.4.2/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-a11y-governance/blob/v0.4.3/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 10,
"templates": 11,
"commands": 3
},
"tags": [
"a11y",
"accessibility",
"bilingual",
"wcag",
"wcag-2-2",
"cefr-b2",
"inclusion",
"include-everyone",
"didactic-comments"
"model-routing"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-07-28T00:00:00Z"
"updated_at": "2026-08-12T00:00:00Z"
},
"agent-parity-governance": {
"name": "Agent Parity Governance",
@@ -97,40 +92,30 @@
"architecture-governance": {
"name": "Architecture Governance",
"id": "architecture-governance",
"version": "0.5.1",
"description": "Adds secure software architecture, resumable remote-transaction boundaries, STRIDE+CAPEC threat modeling, arc42 security cross-cutting concepts, S-ADRs, Zero Trust applicability, OWASP SAMM governance, BSI C3A cloud autonomy, BSI C5 cloud compliance assurance, and audit-ready Spec Kit run evidence.",
"version": "0.5.2",
"description": "Adds secure architecture, STRIDE/CAPEC threat modeling, arc42/S-ADR guidance, Zero Trust, SAMM, BSI cloud assurance, audit evidence, and provider-neutral model routing.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-architecture-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-architecture-governance/archive/refs/tags/v0.5.1.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-architecture-governance/archive/refs/tags/v0.5.2.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-architecture-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-architecture-governance/blob/v0.5.1/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-architecture-governance/blob/v0.5.2/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 13,
"templates": 14,
"commands": 3
},
"tags": [
"architecture",
"governance",
"threat-modeling",
"stride",
"capec",
"arc42",
"adr",
"zero-trust",
"samm",
"isaqb",
"cloud",
"sovereignty",
"c3a",
"c5",
"assurance"
"model-routing"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-07-23T00:00:00Z"
"updated_at": "2026-08-11T00:00:00Z"
},
"autonomous-run-governance": {
"name": "Autonomous Run Governance",
@@ -243,19 +228,19 @@
"cross-platform-governance": {
"name": "Cross-Platform Governance",
"id": "cross-platform-governance",
"version": "0.2.1",
"description": "Adds Bash/PowerShell and read-only check parity, root-path and native-override review, Unix man pages, bilingual help, Verb-Noun discipline, and audit-ready evidence.",
"version": "0.2.2",
"description": "Adds Bash/PowerShell parity, read-only checks, path and native-override review, Unix man pages, bilingual PowerShell help, and provider-neutral model routing.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/archive/refs/tags/v0.2.1.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/archive/refs/tags/v0.2.2.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/blob/v0.2.1/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-cross-platform-governance/blob/v0.2.2/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 8,
"templates": 9,
"commands": 3
},
"tags": [
@@ -263,15 +248,10 @@
"governance",
"bash",
"powershell",
"man-page",
"cmdlet",
"verb-noun",
"windows",
"macos",
"linux"
"model-routing"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-07-23T00:00:00Z"
"updated_at": "2026-08-12T00:00:00Z"
},
"explicit-task-dependencies": {
"name": "Explicit Task Dependencies",
@@ -454,34 +434,30 @@
"isaqb-architecture-governance": {
"name": "iSAQB Architecture Governance",
"id": "isaqb-architecture-governance",
"version": "0.2.1",
"description": "Adds iSAQB/CPSA-F and arc42 architecture governance with audit-ready evidence for goals, views, resumability, partial-failure scenarios, ADRs, risks, and technical debt.",
"version": "0.2.2",
"description": "Adds iSAQB/CPSA-F and arc42 architecture governance, architecture views, quality scenarios, ADRs, risks, technical-debt evidence, and provider-neutral model routing.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/archive/refs/tags/v0.2.1.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/archive/refs/tags/v0.2.2.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/blob/v0.2.1/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-isaqb-architecture-governance/blob/v0.2.2/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 13,
"templates": 14,
"commands": 3
},
"tags": [
"architecture",
"governance",
"isaqb",
"cpsa-f",
"arc42",
"adr",
"quality-attributes",
"architecture-views",
"technical-debt"
"model-routing"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-07-23T00:00:00Z"
"updated_at": "2026-08-11T00:00:00Z"
},
"jira": {
"name": "Jira Issue Tracking",
@@ -609,7 +585,7 @@
"documentation": "https://github.com/hindermath/spec-kit-preset-parallel-autonomous-run-governance/blob/v0.2.4/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.3"
"speckit_version": ">=0.8.3"
},
"provides": {
"templates": 9,
@@ -692,52 +668,30 @@
"security-governance": {
"name": "Security Governance",
"id": "security-governance",
"version": "0.6.1",
"description": "Adds memory-safe-language and secure-coding governance, exact-head and security-gate evidence, provider-failure classification, ASVS, supply-chain transparency, and EU regulatory screening.",
"version": "0.6.2",
"description": "Adds memory-safe-language and secure-coding governance, exact-head security evidence, ASVS, supply-chain transparency, EU regulatory screening, and provider-neutral model routing.",
"author": "Thorsten Hindermann",
"repository": "https://github.com/hindermath/spec-kit-preset-security-governance",
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.6.1.zip",
"download_url": "https://github.com/hindermath/spec-kit-preset-security-governance/archive/refs/tags/v0.6.2.zip",
"homepage": "https://github.com/hindermath/spec-kit-preset-security-governance",
"documentation": "https://github.com/hindermath/spec-kit-preset-security-governance/blob/v0.6.1/README.md",
"documentation": "https://github.com/hindermath/spec-kit-preset-security-governance/blob/v0.6.2/README.md",
"license": "MIT",
"requires": {
"speckit_version": ">=0.8.0"
},
"provides": {
"templates": 14,
"templates": 15,
"commands": 3
},
"tags": [
"security",
"governance",
"msl",
"ssdf",
"asvs",
"supply-chain",
"sbom",
"ai-sbom",
"vex",
"slsa",
"cwe-top-25",
"secure-coding",
"rust",
"go",
"swift",
"java",
"kotlin",
"python",
"typescript",
"g7",
"bsi",
"cra",
"cyber-resilience-act",
"nis2",
"ai-act",
"dora",
"regulatory"
"supply-chain",
"model-routing"
],
"created_at": "2026-04-27T00:00:00Z",
"updated_at": "2026-07-23T00:00:00Z"
"updated_at": "2026-08-10T00:00:00Z"
},
"sicario-core": {
"name": "SicarioSpec Core",
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "specify-cli"
version = "0.16.2.dev0"
version = "0.16.3"
description = "Specify CLI, part of GitHub Spec Kit. A tool to bootstrap your projects for Spec-Driven Development (SDD)."
readme = "README.md"
requires-python = ">=3.11"
+1 -1
View File
@@ -213,7 +213,7 @@ def handle_vscode_settings(sub_item, dest_file, rel_path, verbose=False, tracker
shutil.copy2(sub_item, dest_file)
log("Copied (no existing settings.json):", "blue")
except Exception as e:
except (OSError, ValueError, KeyError) as e:
log(f"Warning: Could not merge settings: {e}", "yellow")
if not dest_file.exists():
shutil.copy2(sub_item, dest_file)
@@ -77,6 +77,16 @@ def resolve_install_plan(
# FR-019: integration-compatibility — a bundle that pins a different
# integration than the project's active one halts (no silent change).
#
# A blank integration arrives as ``""``, not ``None`` — which is not a usable
# integration id but satisfied NEITHER guard below (the first is a truthiness
# test, the second an ``is None`` test), so a pinned bundle was silently
# adopted: precisely the outcome this guard exists to prevent. Treat blank as
# indeterminate, and strip first like the writer
# (``integration_state.clean_integration_key``) so a padded value is not
# reported as clashing with itself.
if active_integration is not None:
active_integration = active_integration.strip() or None
effective_integration = active_integration
if manifest.integration is not None:
required = manifest.integration.id
+1 -1
View File
@@ -14,8 +14,8 @@ from pathlib import Path
import typer
from rich.markup import escape as _escape_markup
from ..._console import console, err_console
from ..._download_security import MAX_DOWNLOAD_BYTES, read_response_limited
from ..._console import console, err_console
from ...bundler import BundlerError
from ...bundler.lib.project import (
active_integration,
+5
View File
@@ -4712,6 +4712,7 @@ class HookExecutor:
kimi_skill_mode = selected_ai == "kimi"
cline_mode = selected_ai == "cline"
forge_mode = selected_ai == "forge"
junie_mode = selected_ai == "junie"
skill_name = self._skill_name_from_command(command_id)
if dollar_skill_mode and skill_name:
@@ -4726,6 +4727,10 @@ class HookExecutor:
from ..integrations.forge import format_forge_command_name
return f"/{format_forge_command_name(command_id)}"
if junie_mode:
from ..integrations.junie import format_junie_command_name
return f"/{format_junie_command_name(command_id)}"
use_slash = is_slash_skills_agent(selected_ai, ai_skills_enabled)
@@ -65,7 +65,16 @@ class AlquimiaAIIntegration(SkillsIntegration):
@staticmethod
def inject_argument_hint(content: str, hint: str) -> str:
"""Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter.
"""Insert ``argument-hint`` after the ``description:`` scalar in YAML frontmatter.
A long ``description`` gets folded by the YAML dumper across
indented continuation lines (plain or quoted), and an embedded
paragraph break can add unindented blank lines inside a quoted
scalar. Inserting the new line right after the *first* line of
that scalar — instead of after the whole scalar — either produces
invalid YAML or gets silently absorbed into the description
string (#4044), so every continuation line (indented, or blank)
is skipped first.
Skips injection if ``argument-hint:`` already exists in the
frontmatter to avoid duplicate keys.
@@ -88,15 +97,29 @@ class AlquimiaAIIntegration(SkillsIntegration):
in_fm = False
dash_count = 0
injected = False
for line in lines:
i = 0
n = len(lines)
while i < n:
line = lines[i]
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
in_fm = dash_count == 1
out.append(line)
i += 1
continue
if in_fm and not injected and stripped.startswith("description:"):
out.append(line)
i += 1
# Skip folded/quoted continuation lines before inserting
# so the new key lands after the description scalar ends.
# Blank lines count too: PyYAML emits unindented blank
# lines for embedded "\n\n" inside a quoted scalar.
while i < n and (
lines[i][:1] in (" ", "\t") or lines[i].rstrip("\r\n") == ""
):
out.append(lines[i])
i += 1
# Preserve the exact line-ending style (\r\n vs \n)
if line.endswith("\r\n"):
eol = "\r\n"
@@ -109,6 +132,7 @@ class AlquimiaAIIntegration(SkillsIntegration):
injected = True
continue
out.append(line)
i += 1
return "".join(out)
@staticmethod
@@ -67,7 +67,16 @@ class ClaudeIntegration(SkillsIntegration):
@staticmethod
def inject_argument_hint(content: str, hint: str) -> str:
"""Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter.
"""Insert ``argument-hint`` after the ``description:`` scalar in YAML frontmatter.
A long ``description`` gets folded by the YAML dumper across
indented continuation lines (plain or quoted), and an embedded
paragraph break can add unindented blank lines inside a quoted
scalar. Inserting the new line right after the *first* line of
that scalar — instead of after the whole scalar — either produces
invalid YAML or gets silently absorbed into the description
string (#4044), so every continuation line (indented, or blank)
is skipped first.
Skips injection if ``argument-hint:`` already exists in the
frontmatter to avoid duplicate keys.
@@ -90,15 +99,29 @@ class ClaudeIntegration(SkillsIntegration):
in_fm = False
dash_count = 0
injected = False
for line in lines:
i = 0
n = len(lines)
while i < n:
line = lines[i]
stripped = line.rstrip("\n\r")
if stripped == "---":
dash_count += 1
in_fm = dash_count == 1
out.append(line)
i += 1
continue
if in_fm and not injected and stripped.startswith("description:"):
out.append(line)
i += 1
# Skip past folded/quoted continuation lines of the scalar
# before inserting, so the new key lands after it ends.
# Blank lines count too: PyYAML emits unindented blank
# lines for embedded "\n\n" inside a quoted scalar.
while i < n and (
lines[i][:1] in (" ", "\t") or lines[i].rstrip("\r\n") == ""
):
out.append(lines[i])
i += 1
# Preserve the exact line-ending style (\r\n vs \n)
if line.endswith("\r\n"):
eol = "\r\n"
@@ -111,6 +134,7 @@ class ClaudeIntegration(SkillsIntegration):
injected = True
continue
out.append(line)
i += 1
return "".join(out)
def _render_skill(self, template_name: str, frontmatter: dict[str, Any], body: str) -> str:
@@ -1,6 +1,51 @@
"""Junie integration (JetBrains)."""
from ..base import MarkdownIntegration
from ..manifest import IntegrationManifest
import re
from pathlib import Path
from typing import Any
# Note injected into hook sections so Junie maps dot-notation command
# names (from extensions.yml) to the hyphenated slash commands it uses.
_HOOK_COMMAND_NOTE = (
"- When constructing slash commands from hook command names, "
"replace dots (`.`) with hyphens (`-`). "
"For example, `speckit.git.commit` → `/speckit-git-commit`.\n"
)
def format_junie_command_name(cmd_name: str) -> str:
"""Convert command name to Junie-compatible hyphenated format.
Junie does not allow dots inside of slash-commands.
This function converts dot-notation command names to hyphenated format.
The function is idempotent: already-formatted names are returned unchanged.
Examples:
>>> format_junie_command_name("plan")
'speckit-plan'
>>> format_junie_command_name("speckit.plan")
'speckit-plan'
>>> format_junie_command_name("speckit.git.commit")
'speckit-git-commit'
Args:
cmd_name: Command name in dot notation (speckit.foo.bar),
hyphenated format (speckit-foo-bar), or plain name (foo)
Returns:
Hyphenated command name with 'speckit-' prefix
"""
cmd_name = cmd_name.replace(".", "-")
if not cmd_name.startswith("speckit-"):
cmd_name = f"speckit-{cmd_name}"
return cmd_name
class JunieIntegration(MarkdownIntegration):
@@ -17,5 +62,117 @@ class JunieIntegration(MarkdownIntegration):
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
"inject_name": True,
"format_name": format_junie_command_name,
"invoke_separator": "-",
}
multi_install_safe = True
invoke_separator = "-"
def command_filename(self, template_name: str) -> str:
return format_junie_command_name(template_name) + ".md"
def build_command_invocation(self, command_name: str, args: str = "") -> str:
"""Junie installs hyphenated slash-commands (``/speckit-<name>``), so the
dispatch invocation must match. The inherited MarkdownIntegration default
builds the dotted ``/speckit.<name>``, which references a command Junie
never registered. Reuse the same hyphenation as command_filename /
the injected frontmatter name (see ``format_junie_command_name``),
mirroring the forge integration.
"""
invocation = "/" + format_junie_command_name(command_name)
if args:
invocation = f"{invocation} {args}"
return invocation
def process_template(self, *args, **kwargs):
"""Ensure shared templates render Junie command references with hyphens."""
kwargs.setdefault("invoke_separator", self.invoke_separator)
return super().process_template(*args, **kwargs)
@staticmethod
def _inject_hook_command_note(content: str) -> str:
"""Insert a dot-to-hyphen note before each hook output instruction.
Targets the line ``- For each executable hook, output the following``
and inserts the note on the line before it, matching its indentation.
Skips if the note is already present.
"""
if "replace dots" in content:
return content
def repl(m: re.Match[str]) -> str:
indent = m.group(1)
instruction = m.group(2)
# ``eol`` is empty when the regex matched via ``$`` because the
# instruction was the final line of a file with no trailing
# newline. Default to ``\n`` so the note never collapses onto
# the same line as the instruction.
eol = m.group(3) or "\n"
return (
indent
+ _HOOK_COMMAND_NOTE.rstrip("\n")
+ eol
+ indent
+ instruction
+ eol
)
return re.sub(
r"(?m)^(\s*)(- For each executable hook, output the following[^\r\n]*)(\r\n|\n|$)",
repl,
content,
)
@staticmethod
def _rewrite_handoff_references(content: str) -> str:
"""Replace dot-notation agent references in handoffs with hyphens."""
return re.sub(
r"(?m)^(\s*agent:\s*)(speckit\.[A-Za-z0-9-_]+(?:\.[A-Za-z0-9-_]+)*)",
lambda m: f"{m.group(1)}{format_junie_command_name(m.group(2))}",
content,
)
def post_process_command_content(self, content: str) -> str:
"""Apply Junie-specific transformations to command content.
Overrides the ``IntegrationBase`` hook of the same name so that
``CommandRegistrar.register_commands()`` (which dispatches to
``post_process_command_content``) applies these transforms to
extension/preset command files too, not just core commands.
"""
updated = self._inject_hook_command_note(content)
updated = self._rewrite_handoff_references(updated)
return updated
def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
"""Install Junie commands and apply post-processing transformations."""
created = super().setup(project_root, manifest, parsed_options, **opts)
# Post-process generated command files
dest_dir = self.commands_dest(project_root).resolve()
for path in created:
# Only touch .md files under the commands directory
try:
path.resolve().relative_to(dest_dir)
except ValueError:
continue
if path.suffix != ".md":
continue
content_bytes = path.read_bytes()
content = content_bytes.decode("utf-8")
updated = self.post_process_command_content(content)
if updated != content:
path.write_bytes(updated.encode("utf-8"))
self.record_file_in_manifest(path, project_root, manifest)
return created
+69 -71
View File
@@ -823,25 +823,6 @@ class PresetManager:
return True
def _extension_installed_for_command(self, command_name: str) -> bool:
"""Whether *command_name* may be materialized in this project.
Extension command overrides follow ``speckit.<ext-id>.<cmd-name>``;
they must be skipped everywhere preset artifacts are written —
registration *and* reconciliation — when the extension isn't
installed, or reconciliation would materialize files that
registration refused to track. Core commands (single-dot names,
e.g. ``speckit.specify``) always pass.
"""
parts = command_name.split(".")
if len(parts) >= 3 and parts[0] == "speckit":
ext_id = parts[1]
if not (
self.project_root / ".specify" / "extensions" / ext_id
).is_dir():
return False
return True
def _register_commands(
self,
manifest: PresetManifest,
@@ -870,21 +851,20 @@ class PresetManager:
if not command_templates:
return {}
# Filter out extension command overrides if the extension isn't installed.
filtered = [
cmd
for cmd in command_templates
if self._extension_installed_for_command(cmd["name"])
]
if not filtered:
return {}
# A preset command template always ships its own body, so it is
# self-contained and scaffolds regardless of whether any similarly
# named extension is installed. Namespaced names (speckit.<ns>.<cmd>)
# are treated exactly like short names (speckit.<cmd>) — they are NOT
# filtered out just because ``.specify/extensions/<ns>/`` is absent.
# The only command that cannot be materialized is a composition
# (prepend/append/wrap) with no base layer to compose onto; that case
# is handled per-command below (warn + skip), not by dropping names up
# front.
# Handle composition strategies: resolve composed content for non-replace commands
resolver = PresetResolver(self.project_root)
composed_dir = None
commands_to_register = []
for cmd in filtered:
for cmd in command_templates:
strategy = cmd.get("strategy", "replace")
if strategy != "replace":
# Only pre-compose if this preset is the top composing layer.
@@ -907,13 +887,23 @@ class PresetManager:
"file": f".composed/{cmd['name']}.md",
})
else:
raise PresetValidationError(
f"Command '{cmd['name']}' uses '{strategy}' strategy "
f"but no base command layer exists to compose onto. "
f"Ensure a lower-priority preset, extension, or core "
f"command provides this command before using "
f"composition strategies."
# No base layer to compose onto (e.g. the command it
# would wrap comes from an extension that isn't
# installed). Warn and skip this single command rather
# than aborting the whole install — mirrors the
# "composed is None" branch in
# _reconcile_composed_commands so command-mode and
# reconciliation behave identically.
import warnings
warnings.warn(
f"Command '{cmd['name']}' uses '{strategy}' "
f"strategy but no base command layer exists to "
f"compose onto; skipping. Provide a lower-priority "
f"preset, extension, or core command for it before "
f"using composition strategies.",
stacklevel=2,
)
continue
else:
# Not the top layer — register raw file; reconciliation
# will overwrite with the correct composed/winning content.
@@ -1681,21 +1671,13 @@ class PresetManager:
if not command_names:
return set()
# Never materialize extension-scoped commands whose extension isn't
# installed. Registration (_register_commands / _register_skills)
# already refuses them, so a reconciliation pass writing them would
# create files no registry entry tracks. Filtering here — the single
# chokepoint every install/remove/rescaffold reconciliation funnels
# through — keeps all callers consistent without each one re-applying
# the filter when seeding names from manifest templates.
command_names = [
name
for name in command_names
if self._extension_installed_for_command(name)
]
if not command_names:
return set()
# Every preset-owned command name flows through unchanged. Names are
# NOT filtered by the ``speckit.<ns>.<cmd>`` shape: a self-contained
# preset command scaffolds whether or not a like-named extension is
# installed (parity with _register_commands), and a name whose base
# layer has disappeared must still reach the loop below so its now
# uncomposable stale file gets unregistered. The loop already skips
# names that resolve to no layers at all (``if not layers: continue``).
try:
from ..agents import CommandRegistrar
except ImportError:
@@ -2136,14 +2118,11 @@ class PresetManager:
if not command_names:
return set()
command_names = [
name
for name in command_names
if self._extension_installed_for_command(name)
]
if not command_names:
return set()
# Preset-owned command names are not filtered by the
# ``speckit.<ns>.<cmd>`` shape here either: a self-contained preset
# command renders its skill whether or not a like-named extension is
# installed. The per-name loop below skips anything that doesn't
# resolve to a managed skill directory.
resolver = PresetResolver(self.project_root)
active_skills_dir = self._get_skills_dir()
@@ -2673,21 +2652,17 @@ class PresetManager:
if not command_templates:
return {}
# Filter out extension command overrides if the extension isn't installed,
# matching the same logic used by _register_commands().
filtered = [
cmd
for cmd in command_templates
if self._extension_installed_for_command(cmd["name"])
]
if not filtered:
return {}
# Preset command templates are self-contained and render as skills
# regardless of whether a like-named extension is installed — the same
# rule _register_commands() uses. No ``speckit.<ns>.<cmd>`` name-shape
# filtering; the per-command loop below skips anything without a target
# skill directory.
skills_dir = target_dir if target_dir is not None else self._get_skills_dir()
if not skills_dir:
return {}
resolver = PresetResolver(self.project_root)
from .. import SKILL_DESCRIPTIONS, load_init_options
from ..agents import CommandRegistrar
from ..integrations import get_integration
@@ -2717,7 +2692,7 @@ class PresetManager:
written: List[str] = []
for cmd_tmpl in filtered:
for cmd_tmpl in command_templates:
cmd_name = cmd_tmpl["name"]
cmd_file_rel = cmd_tmpl["file"]
source_file = preset_dir / cmd_file_rel
@@ -2757,6 +2732,29 @@ class PresetManager:
content = source_file.read_text(encoding="utf-8")
frontmatter, body = registrar.parse_frontmatter(content)
# A composition-strategy command (wrap/prepend/append) needs a
# base layer to compose onto. When _register_commands produced no
# composed file for it and the stack still has no base
# (resolve_content is None) — e.g. the command it wraps comes from
# an extension that isn't installed — rendering the raw preset
# fragment as a skill would emit broken output: a literal
# {CORE_TEMPLATE} for wrap, or only the preset's own fragment for
# prepend/append. Skip it here too so command mode and skills mode
# agree (mirrors _register_commands, which skips the same command).
# _register_commands already warned for this command in the same
# pass, so the skip is silent here to avoid a duplicate warning.
effective_strategy = (
cmd_tmpl.get("strategy")
or frontmatter.get("strategy")
or "replace"
)
if (
effective_strategy != "replace"
and not composed_file.exists()
and resolver.resolve_content(cmd_name, "command") is None
):
continue
if frontmatter.get("strategy") == "wrap":
body, core_frontmatter = _substitute_core_template(body, cmd_name, self.project_root, registrar)
frontmatter = dict(frontmatter)
+11 -1
View File
@@ -59,7 +59,15 @@ def preset_list():
console.print(" [cyan]specify preset add <pack-name>[/cyan]")
return
console.print("\n[bold cyan]Installed Presets:[/bold cyan]\n")
# Sort by actual resolution precedence: lower priority number wins, ties
# broken by preset id (matching PresetRegistry.list_by_priority()). This
# keeps the printed order aligned with how presets are composed/resolved.
installed = sorted(
installed,
key=lambda pack: (pack.get("priority", 10), str(pack.get("id", ""))),
)
console.print("\n[bold cyan]Installed Presets[/bold cyan] [dim](in resolution order — highest precedence first)[/dim]\n")
for pack in installed:
status = "[green]enabled[/green]" if pack.get("enabled", True) else "[red]disabled[/red]"
pri = pack.get('priority', 10)
@@ -75,6 +83,8 @@ def preset_list():
console.print(f" [dim]Templates: {pack['template_count']}[/dim]")
console.print()
console.print("[dim]Lower priority number = higher precedence. Ties are broken by preset id (alphabetical).[/dim]")
@preset_app.command("add")
def preset_add(
+7 -2
View File
@@ -4,7 +4,9 @@
**Created**: [DATE]
**Feature**: [Link to spec.md or relevant documentation]
**Note**: This checklist is generated by the `__SPECKIT_COMMAND_CHECKLIST__` command based on feature context and requirements.
**Note**: This custom checklist is generated by the `__SPECKIT_COMMAND_CHECKLIST__` command based on feature context and requirements.
**Review Ownership**: This checklist is a reviewer-owned requirements-quality review artifact. Mark an item `[x]` only when the reviewer determines the requirements-quality criterion is satisfied.
**Marker Semantics**: `[x]` means the criterion has been reviewed and satisfied for requirements quality. It does not mean implementation work is complete.
<!--
============================================================================
@@ -34,7 +36,10 @@
## Notes
- Check items off as completed: `[x]`
- Mark items `[x]` only after review confirms the requirement-quality criterion is satisfied
- Leave items unchecked when they still require clarification, correction, or reviewer evaluation
- `__SPECKIT_COMMAND_IMPLEMENT__` reads checklist checkbox state as a gate and must not modify markers
- `checklists/requirements.md` has a separate built-in lifecycle maintained by `__SPECKIT_COMMAND_SPECIFY__` and `__SPECKIT_COMMAND_CLARIFY__`
- Add comments or findings inline
- Link to relevant resources or documentation
- Items are numbered sequentially for easy reference
+11 -1
View File
@@ -27,6 +27,15 @@ scripts:
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
**Ownership and checkbox lifecycle**:
- Custom checklists generated by this command are reviewer-owned requirements-quality review artifacts.
- `[x]` means the reviewer determined the requirements-quality criterion is satisfied.
- `[x]` does NOT mean implementation work is complete.
- This command generates or appends checklist items; it MUST NOT mark generated items `[x]`.
- An agent may assist with evaluating items only when explicitly asked by the reviewer.
- `checklists/requirements.md` is a separate built-in spec-quality checklist maintained by `__SPECKIT_COMMAND_SPECIFY__` and `__SPECKIT_COMMAND_CLARIFY__`; do not treat that exception as applying to custom checklists generated here.
## User Input
```text
@@ -136,6 +145,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- If file does NOT exist: Create new file and number items starting from CHK001
- If file exists: Append new items to existing file, continuing from the last CHK ID (e.g., if last item is CHK015, start new items at CHK016)
- Never delete or replace existing checklist content - always preserve and append
- Leave every newly generated item unchecked (`[ ]`); checkbox state belongs to the reviewer
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
@@ -245,7 +255,7 @@ You **MUST** consider the user input before proceeding (if not empty).
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
- ✅ "Does the spec define [missing aspect]?"
7. **Structure Reference**: Generate the checklist following the canonical template in `templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
7. **Structure Reference**: Generate the checklist following the canonical template in `templates/checklist-template.md` for title, meta section, category headings, ownership note, notes section, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, an ownership note explaining that `[x]` means reviewer approval of requirements quality, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001, and notes that `__SPECKIT_COMMAND_IMPLEMENT__` reads checklist state but does not modify markers.
8. **Report**: Output full path to checklist file, item count, and summarize whether the run created a new file or appended to an existing one. Summarize:
- Focus areas selected
+16 -13
View File
@@ -54,33 +54,36 @@ You **MUST** consider the user input before proceeding (if not empty).
1. Run `{SCRIPT}` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
- Treat checklist markers as a read-only gate: scan checkbox state, report status, and ask before proceeding when needed; do NOT modify checklist files or markers
- `checklists/requirements.md` is the built-in spec-quality checklist maintained by `__SPECKIT_COMMAND_SPECIFY__` and `__SPECKIT_COMMAND_CLARIFY__`; custom checklists generated by `__SPECKIT_COMMAND_CHECKLIST__` are reviewer-owned requirements-quality review artifacts
- For custom checklists, `[x]` means the reviewer determined the requirements-quality criterion is satisfied; it does NOT mean implementation work is complete
- Scan all checklist files in the checklists/ directory
- For each checklist, count:
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
- Completed items: Lines matching `- [X]` or `- [x]`
- Incomplete items: Lines matching `- [ ]`
- Checked items: Lines matching `- [X]` or `- [x]`
- Unchecked items: Lines matching `- [ ]`
- Create a status table:
```text
| Checklist | Total | Completed | Incomplete | Status |
|-----------|-------|-----------|------------|--------|
| ux.md | 12 | 12 | 0 | ✓ PASS |
| test.md | 8 | 5 | 3 | ✗ FAIL |
| security.md | 6 | 6 | 0 | ✓ PASS |
| Checklist | Total | Checked | Unchecked | Status |
|-----------|-------|---------|-----------|--------|
| ux.md | 12 | 12 | 0 | ✓ PASS |
| test.md | 8 | 5 | 3 | ✗ FAIL |
| security.md | 6 | 6 | 0 | ✓ PASS |
```
- Calculate overall status:
- **PASS**: All checklists have 0 incomplete items
- **FAIL**: One or more checklists have incomplete items
- **PASS**: All checklists have 0 unchecked items
- **FAIL**: One or more checklists have unchecked items
- **If any checklist is incomplete**:
- Display the table with incomplete item counts
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
- **If any checklist has unchecked items**:
- Display the table with unchecked item counts
- **STOP** and ask: "Some checklists have unchecked items. Do you want to proceed with implementation anyway? (yes/no)"
- Wait for user response before continuing
- If user says "no" or "wait" or "stop", halt execution
- If user says "yes" or "proceed" or "continue", proceed to step 3
- **If all checklists are complete**:
- **If all checklists are checked**:
- Display the table showing all checklists passed
- Automatically proceed to step 3
+30
View File
@@ -1010,3 +1010,33 @@ def test_bundle_info_resolves_ghes_browser_release_url(project: Path):
payload = json.loads(result.output)
assert payload["id"] == "demo-bundle"
def test_bundle_download_rejects_oversized_response(project: Path, monkeypatch):
"""Bundle download rejects responses exceeding MAX_DOWNLOAD_BYTES."""
# Monkeypatch to a small limit so the test is fast and low-memory.
monkeypatch.setattr(
"specify_cli.commands.bundle.MAX_DOWNLOAD_BYTES", 100
)
api_asset_url = "https://api.github.com/repos/org/repo/releases/assets/99"
def fake_open_url(url, timeout=None, extra_headers=None, redirect_validator=None):
# Return a response that exceeds 100 bytes.
return FakeBundleResponse(b"x" * 200, url=api_asset_url)
catalog = project / "catalog.json"
write_catalog_file(
catalog,
{"demo-bundle": catalog_entry_dict("demo-bundle", download_url=api_asset_url)},
)
_make_catalog_config(catalog, project)
with patch("specify_cli.authentication.http.open_url", side_effect=fake_open_url):
result = runner.invoke(app, ["bundle", "info", "demo-bundle", "--json"])
# Must fail with a size-limit error, not an unhandled traceback.
assert result.exit_code == 1
# Rich may wrap the message across lines; normalise whitespace before checking.
output_flat = " ".join(result.output.split())
assert "exceeds maximum size of 100 bytes" in output_flat
@@ -471,6 +471,101 @@ class TestAlquimiaArgumentHints:
hint_count = sum(1 for ln in lines if ln.startswith("argument-hint:"))
assert hint_count == 1
def test_inject_argument_hint_survives_folded_description(self):
"""A long description folded across lines must not corrupt the YAML (#4044).
A description long enough for the YAML dumper to fold it into a
multi-line plain scalar previously had ``argument-hint:`` spliced
into the *middle* of that scalar, producing invalid YAML.
"""
from specify_cli.integrations.alquimia import AlquimiaAIIntegration
frontmatter = {
"name": "speckit-specify",
"description": (
"Create or update the feature specification from a natural "
"language feature description. Also accepts an issue URL "
"resolved via gh CLI (demo customization)."
),
"compatibility": "Requires spec-kit project structure with .specify/ directory",
}
frontmatter_text = yaml.safe_dump(
frontmatter, sort_keys=False, allow_unicode=True
).strip()
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
assert "\n " in content, "fixture description must actually fold across lines"
result = AlquimiaAIIntegration.inject_argument_hint(
content, "Describe the feature"
)
parsed = yaml.safe_load(result.split("---")[1])
assert parsed["argument-hint"] == "Describe the feature"
assert parsed["description"] == frontmatter["description"]
def test_inject_argument_hint_survives_quoted_folded_description(self):
"""A folded description forced into quotes must not absorb the hint (#4044)."""
from specify_cli.integrations.alquimia import AlquimiaAIIntegration
frontmatter = {
"name": "speckit-specify",
"description": (
"Create or update the feature specification from a natural "
"language feature description. Also accepts a GitHub "
"issue/PR URL or #N reference resolved via gh CLI (demo)."
),
"compatibility": "Requires spec-kit project structure with .specify/ directory",
}
frontmatter_text = yaml.safe_dump(
frontmatter, sort_keys=False, allow_unicode=True
).strip()
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
assert "\n " in content, "fixture description must actually fold across lines"
result = AlquimiaAIIntegration.inject_argument_hint(
content, "Describe the feature"
)
parsed = yaml.safe_load(result.split("---")[1])
assert parsed["argument-hint"] == "Describe the feature"
assert parsed["description"] == frontmatter["description"]
def test_inject_argument_hint_survives_multi_paragraph_description(self):
"""A description with an embedded blank line must not absorb the hint.
PyYAML serializes an embedded ``\\n\\n`` inside a quoted scalar as
unindented blank lines, not indented ones, so a fix that only skips
indented continuation lines still fails on this case.
"""
from specify_cli.integrations.alquimia import AlquimiaAIIntegration
frontmatter = {
"name": "speckit-specify",
"description": (
"First paragraph of a fairly long description that will "
"need to wrap across multiple lines when dumped by PyYAML."
"\n\n"
"Second paragraph continues the description after a blank "
"line separator to force embedded newlines in the scalar."
),
"compatibility": "Requires spec-kit project structure with .specify/ directory",
}
frontmatter_text = yaml.safe_dump(
frontmatter, sort_keys=False, allow_unicode=True
).strip()
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
assert "\n\n" in frontmatter_text, (
"fixture must produce a blank continuation line"
)
result = AlquimiaAIIntegration.inject_argument_hint(
content, "Describe the feature"
)
parsed = yaml.safe_load(result.split("---")[1])
assert parsed["argument-hint"] == "Describe the feature"
assert parsed["description"] == frontmatter["description"]
class TestAlquimiaDisableModelInvocation:
"""Verify disable-model-invocation is false for Alquimia skills."""
@@ -451,6 +451,93 @@ class TestClaudeArgumentHints:
hint_count = sum(1 for ln in lines if ln.startswith("argument-hint:"))
assert hint_count == 1
def test_inject_argument_hint_survives_folded_description(self):
"""A long description folded across lines must not corrupt the YAML (#4044).
A description long enough for the YAML dumper to fold it into a
multi-line plain scalar previously had ``argument-hint:`` spliced
into the *middle* of that scalar, producing invalid YAML.
"""
from specify_cli.integrations.claude import ClaudeIntegration
frontmatter = {
"name": "speckit-specify",
"description": (
"Create or update the feature specification from a natural "
"language feature description. Also accepts an issue URL "
"resolved via gh CLI (demo customization)."
),
"compatibility": "Requires spec-kit project structure with .specify/ directory",
}
frontmatter_text = yaml.safe_dump(
frontmatter, sort_keys=False, allow_unicode=True
).strip()
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
assert "\n " in content, "fixture description must actually fold across lines"
result = ClaudeIntegration.inject_argument_hint(content, "Describe the feature")
parsed = yaml.safe_load(result.split("---")[1])
assert parsed["argument-hint"] == "Describe the feature"
assert parsed["description"] == frontmatter["description"]
def test_inject_argument_hint_survives_quoted_folded_description(self):
"""A folded description forced into quotes must not absorb the hint (#4044)."""
from specify_cli.integrations.claude import ClaudeIntegration
frontmatter = {
"name": "speckit-specify",
"description": (
"Create or update the feature specification from a natural "
"language feature description. Also accepts a GitHub "
"issue/PR URL or #N reference resolved via gh CLI (demo)."
),
"compatibility": "Requires spec-kit project structure with .specify/ directory",
}
frontmatter_text = yaml.safe_dump(
frontmatter, sort_keys=False, allow_unicode=True
).strip()
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
assert "\n " in content, "fixture description must actually fold across lines"
result = ClaudeIntegration.inject_argument_hint(content, "Describe the feature")
parsed = yaml.safe_load(result.split("---")[1])
assert parsed["argument-hint"] == "Describe the feature"
assert parsed["description"] == frontmatter["description"]
def test_inject_argument_hint_survives_multi_paragraph_description(self):
"""A description with an embedded blank line must not absorb the hint.
PyYAML serializes an embedded ``\\n\\n`` inside a quoted scalar as
unindented blank lines, not indented ones, so a fix that only skips
indented continuation lines still fails on this case.
"""
from specify_cli.integrations.claude import ClaudeIntegration
frontmatter = {
"name": "speckit-specify",
"description": (
"First paragraph of a fairly long description that will "
"need to wrap across multiple lines when dumped by PyYAML."
"\n\n"
"Second paragraph continues the description after a blank "
"line separator to force embedded newlines in the scalar."
),
"compatibility": "Requires spec-kit project structure with .specify/ directory",
}
frontmatter_text = yaml.safe_dump(
frontmatter, sort_keys=False, allow_unicode=True
).strip()
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
assert "\n\n" in frontmatter_text, "fixture must produce a blank continuation line"
result = ClaudeIntegration.inject_argument_hint(content, "Describe the feature")
parsed = yaml.safe_load(result.split("---")[1])
assert parsed["argument-hint"] == "Describe the feature"
assert parsed["description"] == frontmatter["description"]
class TestClaudeDisableModelInvocation:
"""Verify disable-model-invocation is false for Claude skills."""
@@ -1,10 +1,229 @@
"""Tests for JunieIntegration."""
import os
import pytest
from specify_cli.integrations import get_integration
from specify_cli.integrations.junie import format_junie_command_name
from .test_integration_base_markdown import MarkdownIntegrationTests
class TestJunieCommandNameFormatter:
"""Test the junie command name formatter."""
def test_simple_name_without_prefix(self):
"""Test formatting a simple name without 'speckit.' prefix."""
assert format_junie_command_name("plan") == "speckit-plan"
assert format_junie_command_name("tasks") == "speckit-tasks"
assert format_junie_command_name("specify") == "speckit-specify"
def test_name_with_speckit_prefix(self):
"""Test formatting a name that already has 'speckit.' prefix."""
assert format_junie_command_name("speckit.plan") == "speckit-plan"
assert format_junie_command_name("speckit.tasks") == "speckit-tasks"
def test_extension_command_name(self):
"""Test formatting extension command names with dots."""
assert (
format_junie_command_name("speckit.my-extension.example")
== "speckit-my-extension-example"
)
assert (
format_junie_command_name("my-extension.example")
== "speckit-my-extension-example"
)
def test_idempotent_already_hyphenated(self):
"""Test that already-hyphenated names are returned unchanged (idempotent)."""
assert format_junie_command_name("speckit-plan") == "speckit-plan"
assert (
format_junie_command_name("speckit-my-extension-example")
== "speckit-my-extension-example"
)
class TestJunieIntegration(MarkdownIntegrationTests):
KEY = "junie"
FOLDER = ".junie/"
COMMANDS_SUBDIR = "commands"
REGISTRAR_DIR = ".junie/commands"
@pytest.mark.parametrize(
"cmd_name, expected_filename",
[
("plan", "speckit-plan.md"),
("speckit.plan", "speckit-plan.md"),
("speckit.git.commit", "speckit-git-commit.md"),
("speckit", "speckit-speckit.md"),
("speckitfoo", "speckit-speckitfoo.md"),
],
)
def test_junie_command_filename(self, cmd_name, expected_filename):
"""Verify junie uses hyphenated filenames."""
junie = get_integration("junie")
assert junie.command_filename(cmd_name) == expected_filename
def test_junie_invoke_separator(self):
"""Verify junie uses hyphen as invoke separator."""
junie = get_integration("junie")
assert junie.invoke_separator == "-"
assert junie.registrar_config["invoke_separator"] == "-"
def test_junie_name_injection_and_formatting(self):
"""Verify junie has inject_name and format_name configured."""
junie = get_integration("junie")
assert junie.registrar_config["inject_name"] is True
assert junie.registrar_config[
"format_name"] == format_junie_command_name
def test_junie_handoff_rewrite(self):
"""Verify junie rewrites agent: speckit.foo to agent: speckit-foo."""
junie = get_integration("junie")
content = "---\nagent: speckit.plan\n---\n"
rewritten = junie._rewrite_handoff_references(content)
assert rewritten == "---\nagent: speckit-plan\n---\n"
def test_junie_hook_instruction_injection(self):
"""Verify junie injects the dot-to-hyphen note for hooks."""
junie = get_integration("junie")
content = "- For each executable hook, output the following:\n"
injected = junie._inject_hook_command_note(content)
assert "replace dots (`.`) with hyphens (`-`)" in injected
assert "- For each executable hook, output the following:" in injected
def test_junie_hook_instruction_injection_no_trailing_newline(self):
"""Note must not collapse onto the instruction line when the
instruction is the final line with no trailing newline.
The injection regex matches the end-of-line via ``(\\r\\n|\\n|$)``, so
the captured ``eol`` is empty on a file's last line that lacks a
trailing newline. Without an ``or "\\n"`` fallback the note text and
the instruction are emitted on the same line.
"""
junie = get_integration("junie")
content = "- For each executable hook, output the following:" # no trailing \n
injected = junie._inject_hook_command_note(content)
assert "replace dots (`.`) with hyphens (`-`)" in injected
# Instruction stays on its own line rather than being mashed onto the note.
assert "\n- For each executable hook, output the following:" in injected
# -- Overrides for MarkdownIntegrationTests ---------------------------
def test_setup_creates_files(self, tmp_path):
from specify_cli.integrations.manifest import IntegrationManifest
i = get_integration(self.KEY)
m = IntegrationManifest(self.KEY, tmp_path)
created = i.setup(tmp_path, m)
assert len(created) > 0
cmd_files = [
f
for f in created
if "scripts" not in f.parts
and f.suffix == ".md"
]
for f in cmd_files:
assert f.exists()
assert f.name.startswith("speckit-")
assert f.name.endswith(".md")
specify_file = next(
(f for f in cmd_files if f.name == "speckit-specify.md"), None
)
assert specify_file is not None
specify_contents = specify_file.read_text(encoding="utf-8")
assert "/speckit-plan" in specify_contents
assert "/speckit.plan" not in specify_contents
def test_integration_flag_creates_files(self, tmp_path):
from typer.testing import CliRunner
from specify_cli import app
project = tmp_path / f"int-{self.KEY}"
project.mkdir()
old_cwd = os.getcwd()
try:
os.chdir(project)
runner = CliRunner()
result = runner.invoke(
app,
[
"init",
"--here",
"--integration",
self.KEY,
"--script",
"sh",
"--ignore-agent-tools",
],
catch_exceptions=False,
)
finally:
os.chdir(old_cwd)
assert result.exit_code == 0
i = get_integration(self.KEY)
cmd_dir = i.commands_dest(project)
assert cmd_dir.is_dir()
commands = sorted(cmd_dir.glob("speckit-*"))
assert len(commands) > 0
def _expected_files(self, script_variant: str) -> list[str]:
"""Override to expect hyphenated speckit- prefix."""
i = get_integration(self.KEY)
cmd_dir = i.registrar_config["dir"]
files = []
# Command files
for stem in (
self.COMMANDS_SUBDIR_STEMS
if hasattr(self, "COMMANDS_SUBDIR_STEMS")
else self.COMMAND_STEMS
):
files.append(f"{cmd_dir}/speckit-{stem.replace('.', '-')}.md")
# Framework files
files.append(".specify/integration.json")
files.append(".specify/init-options.json")
files.append(f".specify/integrations/{self.KEY}.manifest.json")
files.append(".specify/integrations/speckit.manifest.json")
files.append(".specify/.gitignore")
if script_variant == "sh":
for name in [
"check-prerequisites.sh",
"common.sh",
"create-new-feature.sh",
"resolve-template.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",
"resolve-template.ps1",
"setup-plan.ps1",
"setup-tasks.ps1",
]:
files.append(f".specify/scripts/powershell/{name}")
for name in [
"checklist-template.md",
"constitution-template.md",
"plan-template.md",
"spec-template.md",
"tasks-template.md",
]:
files.append(f".specify/templates/{name}")
files.append(".specify/memory/.constitution-template.json")
files.append(".specify/memory/constitution.md")
# Bundled workflow
files.append(".specify/workflows/speckit/workflow.yml")
files.append(".specify/workflows/workflow-registry.json")
return sorted(files)
+24
View File
@@ -1,5 +1,7 @@
import stat
import pytest
from specify_cli import merge_json_files
from specify_cli import handle_vscode_settings
@@ -188,3 +190,25 @@ def test_handle_vscode_settings_preserves_mode_on_atomic_write(tmp_path):
after_mode = stat.S_IMODE(dest_file.stat().st_mode)
assert after_mode == before_mode
def test_handle_vscode_settings_propagates_programming_errors(tmp_path):
"""Unexpected programming errors (TypeError) must propagate, not be silently swallowed."""
vscode_dir = tmp_path / ".vscode"
vscode_dir.mkdir()
dest_file = vscode_dir / "settings.json"
dest_file.write_text('{"a": 1}\n', encoding="utf-8")
template_file = tmp_path / "template_settings.json"
template_file.write_text('{"b": 2}\n', encoding="utf-8")
import specify_cli._utils as utils_mod
original_merge = utils_mod.merge_json_files
utils_mod.merge_json_files = lambda *a, **kw: (_ for _ in ()).throw(TypeError("boom"))
try:
with pytest.raises(TypeError):
handle_vscode_settings(
template_file, dest_file, "settings.json",
verbose=False, tracker=None,
)
finally:
utils_mod.merge_json_files = original_merge
+202 -39
View File
@@ -4260,8 +4260,14 @@ class TestSelfTestPreset:
assert manifest.id == "invalid-wrap"
assert manager.registry.is_installed("invalid-wrap")
def test_extension_command_skipped_when_extension_missing(self, project_dir, temp_dir):
"""Test that extension command overrides are skipped if the extension isn't installed."""
def test_selfcontained_namespaced_command_scaffolds_without_extension(self, project_dir, temp_dir):
"""A preset shipping a self-contained ``speckit.<ns>.<cmd>`` command
scaffolds even when no matching extension is installed.
The command template ships its own body, so it is self-contained and
must render just like a short ``speckit.<cmd>`` command. It is not
dropped merely because ``.specify/extensions/fakeext/`` is absent.
"""
claude_dir = project_dir / ".claude" / "skills"
claude_dir.mkdir(parents=True)
@@ -4297,11 +4303,13 @@ class TestSelfTestPreset:
manager = PresetManager(project_dir)
manager.install_from_directory(preset_dir, "0.1.5")
# Extension not installed — command should NOT be registered
cmd_file = claude_dir / "speckit.fakeext.cmd.md"
assert not cmd_file.exists(), "Command registered for missing extension"
# Extension not installed, but the preset ships its own command body —
# it must scaffold (as a native-skill SKILL.md for claude) and be
# tracked in the preset's registered_commands.
skill_file = claude_dir / "speckit-fakeext-cmd" / "SKILL.md"
assert skill_file.exists(), "Self-contained namespaced command was dropped"
metadata = manager.registry.get("ext-override")
assert metadata["registered_commands"] == {}
assert metadata["registered_commands"] != {}
def test_extension_command_registered_when_extension_present(self, project_dir, temp_dir):
"""Test that extension command overrides ARE registered when the extension is installed."""
@@ -6528,17 +6536,16 @@ class TestPresetSkills:
"sanity: the new skills-mode artifact should still be written"
)
def test_rescaffold_skips_extension_commands_when_extension_not_installed(
def test_rescaffold_scaffolds_selfcontained_namespaced_commands(
self, project_dir, temp_dir
):
"""Rescaffold must not materialize extension-scoped commands
(``speckit.<ext>.<cmd>``) when the extension isn't installed.
"""A self-contained ``speckit.<ns>.<cmd>`` preset command scaffolds and
survives rescaffold, even when no matching extension is installed.
``_register_commands`` refuses them, but the rescaffold seeded its
final reconciliation pass with every command template name
unfiltered, so ``_reconcile_composed_commands`` wrote the command
file anyway — an artifact no registry entry tracks (review
3623357358).
The preset ships the command body itself, so it is materialized just
like a short ``speckit.<cmd>`` command — both at install and through a
later reconciliation/rescaffold pass. It is not dropped by the
``speckit.<ns>.<cmd>`` name shape (#4076).
"""
self._write_init_options(project_dir, ai="copilot", ai_skills=False)
commands_dir = project_dir / ".github" / "agents"
@@ -6552,24 +6559,24 @@ class TestPresetSkills:
manager.install_from_directory(preset_dir, "0.1.5")
ext_cmd = commands_dir / "speckit.git.feature.agent.md"
assert not ext_cmd.exists(), (
"sanity: install must not write an extension command when the "
"extension isn't installed"
assert ext_cmd.exists(), (
"sanity: install must scaffold a self-contained namespaced command "
"even when its like-named extension isn't installed"
)
manager.register_enabled_presets_for_agent("copilot")
assert not ext_cmd.exists(), (
"rescaffold must not materialize an extension-scoped command "
"whose extension isn't installed"
assert ext_cmd.exists(), (
"rescaffold must keep the self-contained namespaced command"
)
metadata = manager.registry.get("ext-scoped-preset")
assert not (metadata.get("registered_commands") or {}).get("copilot")
assert (metadata.get("registered_commands") or {}).get("copilot")
def test_rescaffold_skips_extension_skills_when_extension_not_installed(
def test_rescaffold_scaffolds_selfcontained_namespaced_skills(
self, project_dir, temp_dir
):
"""Historical tracking must not recreate a missing extension's skill."""
"""A self-contained ``speckit.<ns>.<cmd>`` preset command renders its
skill even when no matching extension is installed."""
self._write_init_options(project_dir, ai="copilot", ai_skills=True)
skills_dir = project_dir / ".github" / "skills"
skills_dir.mkdir(parents=True)
@@ -6586,28 +6593,80 @@ class TestPresetSkills:
skill_name = "speckit-git-feature"
skill_file = skills_dir / skill_name / "SKILL.md"
assert not skill_file.exists()
manager.registry.update(
"ext-scoped-skill-preset",
{"registered_skills": {"copilot": [skill_name]}},
)
overrides_dir = (
project_dir / ".specify" / "templates" / "overrides"
)
overrides_dir.mkdir(parents=True)
(overrides_dir / "speckit.git.feature.md").write_text(
"---\ndescription: Project override\n---\n\nOverride body\n",
encoding="utf-8",
assert skill_file.exists(), (
"install must render a self-contained namespaced command's skill "
"even when its like-named extension isn't installed"
)
manager.register_enabled_presets_for_agent("copilot")
assert not skill_file.exists(), (
"rescaffold must not materialize an extension-scoped skill "
"whose extension isn't installed"
assert skill_file.exists(), (
"rescaffold must keep the self-contained namespaced command's skill"
)
def test_uncomposable_wrap_command_skips_skill_in_skills_mode(
self, project_dir, temp_dir
):
"""A wrap command with no base layer must not materialize a broken
skill in skills mode.
When ``_register_commands`` skips an uncomposable wrap command (no
base to compose onto — e.g. the command it wraps comes from an
uninstalled extension), ``_register_skills`` must skip it too. Before
this fix, skills mode fell back to the raw preset body and wrote a
SKILL.md containing a literal ``{CORE_TEMPLATE}`` placeholder.
"""
self._write_init_options(project_dir, ai="copilot", ai_skills=True)
skills_dir = project_dir / ".github" / "skills"
skills_dir.mkdir(parents=True)
preset_dir = temp_dir / "uncomposable-wrap"
preset_dir.mkdir()
(preset_dir / "commands").mkdir()
# speckit.git.feature has no core command template and no installed
# extension, so there is no base layer to wrap.
(preset_dir / "commands" / "speckit.git.feature.md").write_text(
"---\ndescription: Wrap\nstrategy: wrap\n---\n\n"
"wrap start\n{CORE_TEMPLATE}\nwrap end\n"
)
manifest_data = {
"schema_version": "1.0",
"preset": {
"id": "uncomposable-wrap",
"name": "uncomposable-wrap",
"version": "1.0.0",
"description": "Test",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"templates": [
{
"type": "command",
"name": "speckit.git.feature",
"file": "commands/speckit.git.feature.md",
"strategy": "wrap",
}
]
},
}
with open(preset_dir / "preset.yml", "w") as f:
yaml.dump(manifest_data, f)
manager = PresetManager(project_dir)
with pytest.warns(UserWarning, match="no base command layer"):
manager.install_from_directory(preset_dir, "0.1.5")
skill_file = skills_dir / "speckit-git-feature" / "SKILL.md"
assert not skill_file.exists(), (
"an uncomposable wrap command must not be rendered as a skill"
)
# Belt-and-suspenders: no artifact anywhere may leak the raw placeholder.
leaked = [
p for p in skills_dir.rglob("*")
if p.is_file() and "{CORE_TEMPLATE}" in p.read_text(encoding="utf-8")
]
assert not leaked, f"literal {{CORE_TEMPLATE}} leaked into {leaked}"
def test_same_mode_partial_command_rescaffold_keeps_skipped_tracking(
self, project_dir, temp_dir
):
@@ -10017,6 +10076,43 @@ class TestPresetSkills:
"claude's real ownership must be preserved in the migrated tracking"
)
def test_short_and_namespaced_commands_scaffold_consistently(
self, project_dir, temp_dir
):
"""A preset's ``speckit.<cmd>`` and ``speckit.<ns>.<cmd>`` commands must
scaffold identically in command mode, with no installed extension.
Regression: the 3-part (``speckit.<ns>.<cmd>``) form was silently
dropped by a name-shape guard whenever ``.specify/extensions/<ns>/``
was absent, even though the preset ships the command body itself. The
2-part form always scaffolded. Both are self-contained and must behave
the same (#4076).
"""
self._write_init_options(project_dir, ai="gemini", ai_skills=False)
gemini_commands_dir = project_dir / ".gemini" / "commands"
gemini_commands_dir.mkdir(parents=True)
short_preset = self._create_command_preset(
temp_dir, "short-cmd", "speckit.newcmd", "Short", "short body",
)
ns_preset = self._create_command_preset(
temp_dir, "ns-cmd", "speckit.fakeext.newcmd", "Namespaced", "ns body",
)
manager = PresetManager(project_dir)
manager.install_from_directory(short_preset, "0.1.5")
manager.install_from_directory(ns_preset, "0.1.5")
short_file = gemini_commands_dir / "speckit.newcmd.toml"
ns_file = gemini_commands_dir / "speckit.fakeext.newcmd.toml"
assert short_file.exists(), "2-part command should scaffold"
assert ns_file.exists(), (
"3-part namespaced command must scaffold too, even without the "
"matching extension installed"
)
assert manager.registry.get("short-cmd")["registered_commands"] != {}
assert manager.registry.get("ns-cmd")["registered_commands"] != {}
class TestPresetSetPriority:
"""Test preset set-priority CLI command."""
@@ -13680,6 +13776,73 @@ class TestInstalledPresetRichMarkup:
assert "[base]" in output, output
assert "[append]" in output, output
class TestPresetListOrdering:
"""``preset list`` must print presets in actual resolution/precedence order.
Regression coverage for #4086: the printed order was registry/insertion
order, so a preset with a *higher* priority number (lower precedence) could
appear before one with a lower number, misleading users about which preset
wins. Output must be sorted by (priority, id) to match
``PresetRegistry.list_by_priority()``.
"""
def _install(self, temp_dir, project_dir, pack_id, priority):
from specify_cli.presets import PresetManager
src = temp_dir / f"src-{pack_id}"
(src / "templates").mkdir(parents=True)
(src / "templates" / "spec-template.md").write_text("# tmpl\n")
(src / "preset.yml").write_text(yaml.dump({
"schema_version": "1.0",
"preset": {
"id": pack_id,
"name": pack_id,
"version": "1.0.0",
"description": "plain description",
},
"requires": {"speckit_version": ">=0.0.1"},
"provides": {"templates": [{
"type": "template",
"name": "spec-template",
"file": "templates/spec-template.md",
}]},
}))
PresetManager(project_dir).install_from_directory(src, "9.9.9", priority)
def _invoke(self, project_dir, args):
from typer.testing import CliRunner
from unittest.mock import patch
from specify_cli import app
with patch.object(Path, "cwd", return_value=project_dir):
return CliRunner().invoke(app, args)
def test_list_sorted_by_priority(self, temp_dir, project_dir):
"""Lower priority number is listed first regardless of install order."""
# Install in an order that does NOT match precedence.
self._install(temp_dir, project_dir, "copilot-sub-agents", priority=100)
self._install(temp_dir, project_dir, "lean", priority=10)
result = self._invoke(project_dir, ["preset", "list"])
assert result.exit_code == 0, result.output
output = strip_ansi(result.output)
# `lean` (priority 10) must appear before `copilot-sub-agents` (100).
assert output.index("(lean)") < output.index("(copilot-sub-agents)"), output
assert "resolution order" in output, output
assert "Ties are broken by preset id" in output, output
def test_list_ties_broken_by_id(self, temp_dir, project_dir):
"""Equal priority ties are broken alphabetically by preset id."""
self._install(temp_dir, project_dir, "zebra", priority=10)
self._install(temp_dir, project_dir, "alpha", priority=10)
result = self._invoke(project_dir, ["preset", "list"])
assert result.exit_code == 0, result.output
output = strip_ansi(result.output)
assert output.index("(alpha)") < output.index("(zebra)"), output
class TestConstitutionSyncPreset:
"""The bundled opt-in ``constitution-sync`` preset re-adds materialization.
+26
View File
@@ -62,6 +62,32 @@ def test_pinned_integration_with_indeterminate_active_fails():
)
@pytest.mark.parametrize("blank", ["", " ", "\t"])
def test_pinned_integration_with_blank_active_fails(blank):
"""A blank active integration is indeterminate, not a match.
The clash guard is a truthiness test and the indeterminate guard is an
`is None` test, so `""` satisfied neither and fell through to
`effective_integration = required` — silently adopting the bundle's pinned
integration, the exact outcome the docstring says the guard prevents.
"""
manifest = _manifest(integration={"id": "claude"})
with pytest.raises(BundlerError, match="could not be determined"):
resolve_install_plan(
manifest, speckit_version="0.11.2", active_integration=blank
)
def test_padded_active_integration_is_not_a_clash_with_itself():
"""A padded value must strip, like the writer's clean_integration_key,
rather than be reported as clashing with its own unpadded form."""
manifest = _manifest(integration={"id": "claude"})
plan = resolve_install_plan(
manifest, speckit_version="0.11.2", active_integration=" claude "
)
assert plan.effective_integration == "claude"
def test_pinned_integration_with_indeterminate_active_allows_explicit_override():
manifest = _manifest(integration={"id": "claude"})
plan = resolve_install_plan(