Summary
There is currently no specify preset update <id> command. The only way to move an installed preset to a new version is preset remove <id> followed by preset add <id>. That workaround has real correctness problems beyond performance, because both remove() and install_from_directory() independently trigger full-stack reconciliation (composed commands/skills, and — when constitution-sync is enabled — memory/constitution.md) whenever the touched preset provides a constitution template. A version bump therefore reconciles the constitution twice: once against the stack with the preset temporarily absent, once against the reinstalled stack — with a real window in between where on-disk state reflects neither the old nor the new version.
This was raised as a valid feature-parity gap in #4415, with a request to open a tracking issue including acceptance criteria for: single/all updates, catalog-only eligibility, preserving priority/enabled state, rollback on failure, and command/skill/constitution reconciliation.
Current behavior (traced in source)
PresetManager.install_from_directory (presets/__init__.py) refuses to install over an already-installed preset id unless force=True, which itself calls self.remove() first — i.e. there is no in-place update path, only teardown+rebuild.
PresetResolver.collect_all_layers / resolve_content recompute composed content from the entire installed-preset priority stack on every reconciliation call — never a diff against a single preset's previous version.
_reconcile_constitution / _materialize_constitution_template unconditionally overwrite .specify/memory/constitution.md (when constitution-sync is enabled and the file is still detected as machine-generated) with no check for whether the resolved content actually changed.
PresetRegistry stores priority and enabled per installed preset, but neither remove() nor install_from_directory() reads the previous entry back when reinstalling — so a manual remove+add workaround also risks losing an explicit --priority (if omitted, it silently falls back to the default of 10) and always resets enabled to True regardless of whether the preset was previously disabled.
- Neither
Summary
There is currently no
specify preset update <id>command. The only way to move an installed preset to a new version ispreset remove <id>followed bypreset add <id>. That workaround has real correctness problems beyond performance, because bothremove()andinstall_from_directory()independently trigger full-stack reconciliation (composed commands/skills, and — whenconstitution-syncis enabled —memory/constitution.md) whenever the touched preset provides a constitution template. A version bump therefore reconciles the constitution twice: once against the stack with the preset temporarily absent, once against the reinstalled stack — with a real window in between where on-disk state reflects neither the old nor the new version.This was raised as a valid feature-parity gap in #4415, with a request to open a tracking issue including acceptance criteria for: single/all updates, catalog-only eligibility, preserving priority/enabled state, rollback on failure, and command/skill/constitution reconciliation.
Current behavior (traced in source)
PresetManager.install_from_directory(presets/__init__.py) refuses to install over an already-installed preset id unlessforce=True, which itself callsself.remove()first — i.e. there is no in-place update path, only teardown+rebuild.PresetResolver.collect_all_layers/resolve_contentrecompute composed content from the entire installed-preset priority stack on every reconciliation call — never a diff against a single preset's previous version._reconcile_constitution/_materialize_constitution_templateunconditionally overwrite.specify/memory/constitution.md(whenconstitution-syncis enabled and the file is still detected as machine-generated) with no check for whether the resolved content actually changed.PresetRegistrystorespriorityandenabledper installed preset, but neitherremove()norinstall_from_directory()reads the previous entry back when reinstalling — so a manual remove+add workaround also risks losing an explicit--priority(if omitted, it silently falls back to the default of 10) and always resetsenabledtoTrueregardless of whether the preset was previously disabled.