Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 60 additions & 4 deletions docs/reference/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,12 @@ specify preset enable <preset_id>
specify preset disable <preset_id>
```

Disable a preset without removing it. Disabled presets are skipped during file resolution but their commands remain registered. Re-enable with `enable`.
Disable a preset without removing it. A disabled preset is skipped during template and script resolution and stops contributing to command and skill resolution. Disabling also reconciles the command and skill artifacts the preset had generated:

- If the disabled preset was the only active provider for a materialized command or skill, that artifact is removed for the active integration.
- If a lower/fallback layer (another preset, an installed and enabled extension, or core) still provides the resource, the artifact is re-materialized from the remaining active layers instead of being deleted.

Disabling never deletes the preset itself — its directory, manifest, and registry entry stay in place, and `specify preset enable` restores its contribution. Artifact cleanup is best-effort: if cleanup fails, the affected provenance is preserved so re-running `specify preset disable` retries it. Cleanup is not part of a project-wide transaction, so a failed attempt can leave a materialized command or skill on disk until the retry. Re-enable with `enable`.

## Set Preset Priority

Expand Down Expand Up @@ -287,10 +292,61 @@ catalogs:

Presets can provide command files, template files (like `plan-template.md`), and script files. Each file name is evaluated independently against the priority stack, so different files can come from different layers.

Templates and scripts are looked up from the stack when Spec Kit needs them. Commands use the same stack for replacement and composition, but are materialized into the active integration's directory only, instead of being re-resolved by agents or written to every detected agent directory (#2948). During preset install, Spec Kit registers command files for the preset being installed against the currently active integration; post-install and post-removal reconciliation then recomputes and writes the effective command content for affected command names based on the active stack. Install and rescaffold remain active-only, but removal may also update previously targeted inactive directories recorded by the removed preset to restore the surviving command or skill layer. A non-active installed integration does not otherwise receive these command files until it becomes the default — `specify integration use <key>` (or `switch <key>`) rescaffolds enabled presets for the newly active integration. Agents do not re-resolve the stack each time they run a command.
Templates and scripts are looked up from the stack when Spec Kit needs them. Commands use the same stack for replacement and composition, but are materialized into the active integration's directory only, instead of being re-resolved by agents or written to every detected agent directory (#2948). During preset install, Spec Kit registers command files for the preset being installed against the currently active integration; post-install, post-enable, post-disable, and post-removal reconciliation then recomputes and writes the effective command content for affected command names based on the active stack. Install and rescaffold remain active-only, but removal may also update previously targeted inactive directories recorded by the removed preset to restore the surviving command or skill layer. A non-active installed integration does not otherwise receive these command files until it becomes the default — `specify integration use <key>` (or `switch <key>`) rescaffolds enabled presets for the newly active integration. Agents do not re-resolve the stack each time they run a command.

By default, files use a **replace** strategy: the first match in the priority stack wins and is used entirely. Templates and commands can also use composition strategies: **prepend** places preset content before lower-priority content, **append** places it after lower-priority content, and **wrap** replaces `{CORE_TEMPLATE}` with lower-priority content. Scripts support **replace** and **wrap**; script wrappers use `$CORE_SCRIPT` as the placeholder.

### Regex Selectors

Instead of enumerating one entry per resource, a `provides.templates` entry can match several resources with a `regex:` prefix on its `name`:

```yaml
provides:
templates:
- type: command
name: 'regex:^speckit\.(plan|tasks|implement)$'
file: commands/workflow-guidance.md
strategy: append

- type: template
name: 'regex:.*-template$'
file: templates/common-policy.md
strategy: prepend

- type: script
name: 'regex:^check-.*$'
file: scripts/check-wrapper.sh
strategy: wrap
```

An entry without the prefix keeps exact-name matching and is unaffected:

```yaml
provides:
templates:
- type: command
name: speckit.plan
file: commands/plan-guidance.md
strategy: append
```

How selectors behave:

- `regex:` selectors are supported for all three resource types — `command`, `template`, and `script`.
- A `name` beginning with `regex:` is compiled as a Python regular expression. The pattern is matched with **full-match** semantics against the logical resource name (for example `speckit.plan`, `plan-template`, or `check-constitution`) — the same name you pass to `specify preset resolve` and see in `specify preset info`. It never matches file paths, directory names, or file extensions.
- A selector is eligible only against concrete resources provided by **lower layers** of the resolution stack: lower-priority presets, installed and enabled extensions, and Spec Kit core (including the bundled core pack in a wheel install). Project-local overrides and other `regex:` declarations are never matched, so selectors cannot chain off one another and cannot be satisfied by an override.
- Every concrete resource a selector matches behaves exactly as though the preset contained a separate exact-name entry with the same `file` and `strategy`. One declaration therefore expands into zero, one, or several concrete resources.
- Matching is recalculated whenever preset, extension, or active-integration state changes (add, remove, enable, disable, and priority changes), so adding or removing a lower layer updates the generated artifacts.
- For `command` entries the matches are expanded to concrete command names **before** registration. Registration, composition, reconciliation, and cleanup all run against those concrete names; the literal `regex:...` string is never stored as a command name in the preset registry, an agent directory, or the `.composed` cache.
- For `template` and `script` entries the selector participates in normal runtime resolution: when Spec Kit asks the stack for a concrete name, matching regex declarations from the ordered presets are considered alongside exact declarations.
- Composition and priority are unchanged. `replace`, `prepend`, `append`, and `wrap` compose against the normal resolution stack, and script entries still support only `replace` and `wrap`.

Zero-match and invalid patterns are handled as follows:

- A `regex:` selector that currently matches no lower-layer resource contributes nothing. Installing such a preset emits a warning; it does **not** fail the installation.
- `specify preset info` lists each selector's current concrete matches nested beneath the declaration, or `No current matches` when there are none.
- An invalid regular expression is rejected during manifest validation, so a preset with a malformed pattern fails to install with a clear validation error rather than at resolution time.

The resolution stack, from highest to lowest precedence:

1. **Project-local overrides** — `.specify/templates/overrides/`
Expand Down Expand Up @@ -363,9 +419,9 @@ Run `specify preset resolve <name>` to trace the resolution stack and see which

### What's the difference between disabling and removing a preset?

**Disabling** (`specify preset disable`) keeps the preset installed but excludes it from future template and script resolution. Previously registered commands remain available in your AI coding agent until preset removal, so use removal when you need command changes to stop taking effect. Disabling is useful for temporarily testing template/script behavior without a preset, or comparing template/script output with and without it. Re-enable anytime with `specify preset enable`.
**Disabling** (`specify preset disable`) keeps the preset installed — its files and registry entry are untouched — but stops it contributing to resolution. Templates and scripts it provided are skipped, and the command and skill artifacts it generated are reconciled against the remaining active layers: an artifact with no other active provider is removed, while one that still has a lower/fallback provider is re-materialized from that provider. This is useful for temporarily testing template/script behavior, or comparing command/skill output with and without a preset, without losing the installed preset. Re-enable anytime with `specify preset enable`, which re-materializes the preset's contribution.

**Removing** (`specify preset remove`) fully uninstalls the preset — deletes its files, unregisters its commands from your AI coding agent, and removes it from the registry.
**Removing** (`specify preset remove`) fully uninstalls the preset — deletes its files, unregisters its commands and skills from your AI coding agent, and removes it from the registry.

### Who maintains presets?

Expand Down
17 changes: 14 additions & 3 deletions src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import hashlib
import os
import re
from collections.abc import Callable
from copy import deepcopy
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional
Expand Down Expand Up @@ -675,6 +676,7 @@ def register_commands(
link_outputs: bool = False,
extension_id: Optional[str] = None,
author: object = "github-spec-kit",
on_output: Callable[[str], None] | None = None,
) -> List[str]:
"""Register commands for a specific agent.

Expand Down Expand Up @@ -922,12 +924,13 @@ def register_commands(
link_outputs,
agent_config,
)
registered.append(cmd_name)
if on_output is not None:
on_output(cmd_name)

if agent_name == "copilot":
self.write_copilot_prompt(project_root, cmd_name)

registered.append(cmd_name)

for alias in aliases:
alias_output_name = self._compute_output_name(
agent_name, alias, agent_config
Expand Down Expand Up @@ -1007,9 +1010,11 @@ def register_commands(
link_outputs,
agent_config,
)
registered.append(alias)
if on_output is not None:
on_output(alias)
if agent_name == "copilot":
self.write_copilot_prompt(project_root, alias)
registered.append(alias)

return registered

Expand Down Expand Up @@ -1130,6 +1135,7 @@ def register_commands_for_all_agents(
extension_id: Optional[str] = None,
only_agent: Optional[str] = None,
author: object = "github-spec-kit",
on_output: Callable[[str, str], None] | None = None,
) -> Dict[str, List[str]]:
"""Register commands for all detected agents in the project.

Expand Down Expand Up @@ -1256,6 +1262,11 @@ def register_commands_for_all_agents(
link_outputs=link_outputs,
extension_id=extension_id,
author=author,
on_output=(
(lambda command, agent=agent_name: on_output(agent, command))
if on_output is not None
else None
),
)
if registered:
results[agent_name] = registered
Expand Down
Loading