Problem Statement
Extensions today can only formally declare commands (plus config, hooks, events) under provides — see ExtensionManifest (src/specify_cli/extensions/__init__.py:606-621) and the validation requiring "at least one command, hook, or event" (:386-387). There is no provides.templates or provides.scripts.
An extension can still ship templates/<name>.md or scripts/<name>.sh files, which the resolver picks up by convention (PresetResolver.collect_all_layers → _find_in_subdirs, src/specify_cli/presets/__init__.py:5417-5455). But those contributions are:
- Undeclared — no
id, name, description; matched purely by filename.
- Forced to
replace (:5452) — no author control, and no way to make the intent explicit.
- Invisible to tooling — not counted or shown by any
list/info command.
This blocks a consumer (e.g. a setup wizard) from enumerating extension-contributed templates/scripts with real metadata, and it means presets and extensions have asymmetric manifest schemas for the same kinds of artifacts.
Proposed Solution
Add opt-in, declarative provides.templates and provides.scripts sections to the extension manifest, mirroring the preset shape (provides.templates with type/name/file/description, src/specify_cli/presets/__init__.py:400-453) — minus any authorable strategy, since extension contributions are always replace (see below).
provides:
commands:
- name: speckit.myext.report
file: commands/speckit.myext.report.md
description: "..."
templates:
- name: myext-template
file: templates/myext-template.md
description: "Report scaffold contributed by myext"
scripts:
- name: myext-collect
file: scripts/bash/myext-collect.sh
description: "Data-collection helper"
runtimes: [bash, powershell, python] # declared, not inferred
Behavior:
- Declared entries are authoritative; convention lookup remains the fallback — same pattern presets already use (
:5380-5383), so nothing breaks for existing extensions.
- New
ExtensionManifest.templates / ExtensionManifest.scripts properties + validation (shape, path-safety via relative_extension_path_violation, name format), consistent with the existing command/preset validators.
- Declared templates/scripts become countable and describable, so downstream
list --json / info --json (tracked separately) can report per-kind provides counts and per-artifact detail.
Strategy: replace-only, not authorable
Extension-contributed templates/scripts are always replace, and strategy is not an authorable field for extensions:
- This matches the resolver's current forced-
replace behavior (:5452) — zero behavior change, just making the implicit rule explicit.
- The dominant extension case is contributing a brand-new artifact nothing else provides (e.g.
speckit.git.feature), where replace simply means "become the base layer."
- Augmenting an existing artifact (
wrap/prepend/append) is deliberately preset territory — presets are the "customize/compose what already exists" layer; extensions are the "add new capabilities" layer. Keeping composition strategies out of extensions preserves that split.
- Rather than accept a
strategy key constrained to a single value (which invites authors to try wrap and hit a validation error), the manifest should ignore/reject any strategy key on extension templates/scripts and document the replace semantics.
Alternatives Considered
- Keep convention-only lookup. Rejected: no metadata, no counts,
replace-only by accident, asymmetric with presets.
- Give extensions the full preset strategy set (
wrap/prepend/append). Rejected — augmentation is preset territory; extensions stay additive/replace to preserve the "presets sit above extensions" layering.
- Accept an authorable
strategy field constrained to replace. Rejected — a one-value field misleads authors; better to omit it entirely and document the rule.
- Infer script runtimes from file extensions on disk. Workable but lossy; an explicit
runtimes list is clearer.
Component
Specify CLI (initialization, commands)
Use Cases
- A wizard/UI enumerates everything an installed extension contributes — commands and templates and scripts — with names, descriptions, and (for scripts) runtimes, read straight from the manifest.
- An extension author wants their shipped template/script to appear in
extension info with a real description instead of being an undeclared file.
- Tooling reports accurate per-kind
provides counts for extensions ({ commands, templates, scripts, hooks }).
Acceptance Criteria
Additional Context
- Related wizard-data assessment: this closes the "extensions can't declare templates/scripts" gap and the
runtimes half of the script-metadata gap.
- Downstream dependents (separate issues):
--json for extension list/info and per-kind provides counts; the specify artifact composition command benefits from the added provenance.
Problem Statement
Extensions today can only formally declare commands (plus
config,hooks,events) underprovides— seeExtensionManifest(src/specify_cli/extensions/__init__.py:606-621) and the validation requiring "at least one command, hook, or event" (:386-387). There is noprovides.templatesorprovides.scripts.An extension can still ship
templates/<name>.mdorscripts/<name>.shfiles, which the resolver picks up by convention (PresetResolver.collect_all_layers→_find_in_subdirs,src/specify_cli/presets/__init__.py:5417-5455). But those contributions are:id,name,description; matched purely by filename.replace(:5452) — no author control, and no way to make the intent explicit.list/infocommand.This blocks a consumer (e.g. a setup wizard) from enumerating extension-contributed templates/scripts with real metadata, and it means presets and extensions have asymmetric manifest schemas for the same kinds of artifacts.
Proposed Solution
Add opt-in, declarative
provides.templatesandprovides.scriptssections to the extension manifest, mirroring the preset shape (provides.templateswithtype/name/file/description,src/specify_cli/presets/__init__.py:400-453) — minus any authorablestrategy, since extension contributions are alwaysreplace(see below).Behavior:
:5380-5383), so nothing breaks for existing extensions.ExtensionManifest.templates/ExtensionManifest.scriptsproperties + validation (shape, path-safety viarelative_extension_path_violation, name format), consistent with the existing command/preset validators.list --json/info --json(tracked separately) can report per-kindprovidescounts and per-artifact detail.Strategy: replace-only, not authorable
Extension-contributed templates/scripts are always
replace, andstrategyis not an authorable field for extensions:replacebehavior (:5452) — zero behavior change, just making the implicit rule explicit.speckit.git.feature), wherereplacesimply means "become the base layer."wrap/prepend/append) is deliberately preset territory — presets are the "customize/compose what already exists" layer; extensions are the "add new capabilities" layer. Keeping composition strategies out of extensions preserves that split.strategykey constrained to a single value (which invites authors to trywrapand hit a validation error), the manifest should ignore/reject anystrategykey on extension templates/scripts and document the replace semantics.Alternatives Considered
replace-only by accident, asymmetric with presets.wrap/prepend/append). Rejected — augmentation is preset territory; extensions stay additive/replaceto preserve the "presets sit above extensions" layering.strategyfield constrained toreplace. Rejected — a one-value field misleads authors; better to omit it entirely and document the rule.runtimeslist is clearer.Component
Specify CLI (initialization, commands)
Use Cases
extension infowith a real description instead of being an undeclared file.providescounts for extensions ({ commands, templates, scripts, hooks }).Acceptance Criteria
provides.templatesandprovides.scriptswithname/file/description, andruntimesfor scripts.ExtensionManifestexposestemplatesandscriptsproperties with validation parity to existing command/preset validation (shape, relative-path safety, name format,runtimestype-check).strategynot authorable: the schema does not accept astrategyfield on extension templates/scripts; a presentstrategykey is ignored or rejected with a clear message. Resolution continues to treat extension layers asreplace(:5452).:386-387is relaxed).schema_versionimpact decided (additive under1.0vs. bump to1.1).strategykey), resolver authoritative-vs-convention precedence, runtimes parsing.AGENTS.mdandextensions/EXTENSION-API-REFERENCE.md(manifest schema section), including the replace-only rule.Additional Context
runtimeshalf of the script-metadata gap.--jsonforextension list/infoand per-kindprovidescounts; thespecify artifactcomposition command benefits from the added provenance.